Compare commits

..
67 Commits
Author SHA1 Message Date
Alex Xu 66c6b8611a bump to 11.1.0 2026-09-03 23:06:40 -07:00
dependabot[bot]andpublic 1b3b619491 Bump signpath/github-action-submit-signing-request from 2.2 to 2.3 (#1418)
Bumps [signpath/github-action-submit-signing-request](https://github.com/signpath/github-action-submit-signing-request) from 2.2 to 2.3.
- [Release notes](https://github.com/signpath/github-action-submit-signing-request/releases)
- [Commits](https://github.com/signpath/github-action-submit-signing-request/compare/v2.2...v2.3)

---
updated-dependencies:
- dependency-name: signpath/github-action-submit-signing-request
  dependency-version: '2.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>
2026-09-03 21:59:40 -07:00
dependabot[bot]andpublic fb784a2eb4 Bump actions/setup-node from 6 to 7 (#1401)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  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>
2026-09-03 21:57:10 -07:00
dependabot[bot]andpublic f2deb1061f Bump actions/setup-python from 6 to 7 (#1408)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  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>
2026-09-03 21:56:56 -07:00
Alex Xu 3be76ec89f add legacy extract warning (#1433) 2026-09-03 21:52:27 -07:00
Alex Xu 38fa13610f A "Covers" folder next to source files defines custom covers (#1432)
* A "Covers" folder next to source files defines custom covers

* fix case with output folder unchecked
2026-09-03 15:00:07 -07:00
Alex Xu 6d3bd0296c add folder output to readme 2026-09-03 10:31:36 -07:00
Alex Xu 9decf0e68e add wallpaper mode for KOreader (#1431)
* add wallpaper mode for KOreader

* fix wallpaper interactions with no rotate
2026-09-03 10:21:08 -07:00
Alex Xu dc2cc8e7b8 rename folder to folder of images 2026-09-02 19:49:13 -07:00
Alex Xu 55468019dd add folder output option (#1430) 2026-09-02 16:56:42 -07:00
Alex Xu 8f191e604b replace more images 2026-08-30 18:12:27 -07:00
Alex Xu 924f83d976 use smaller images 2026-08-30 18:09:30 -07:00
Alex Xu 52d52ff323 Update README.md 2026-08-29 16:21:41 -07:00
Alex Xu e93be5cf68 Add files via upload 2026-08-29 16:21:20 -07:00
Alex Xu 4cdbb4beef bump to 11.0.3 2026-08-28 14:52:42 -07:00
Alex Xu 2c9056c524 label spread preview default is high quality (#1429) 2026-08-28 14:46:18 -07:00
Alex Xu a503f54f9f Spread labels support left to right (#1428) 2026-08-28 14:38:22 -07:00
Alex Xu fc331d4e19 bump to 11.0.2 2026-08-26 13:05:44 -07:00
Alex Xu adac558aab shift click label spreads for HQ preview (#1427) 2026-08-26 13:05:14 -07:00
Alex Xu c8cf50da0a fix spread label when it's the first page (#1426) 2026-08-26 11:40:25 -07:00
Alex Xu f38f1c45b2 update README.md 2026-08-26 09:25:07 -07:00
Alex Xu bd0328a12f bump to 11.0.1 2026-08-02 10:47:25 -07:00
Alex Xu 846a755761 fix label feature on Windows (#1413) 2026-08-01 15:58:45 -07:00
Alex Xu ff80d275e6 bump to 11.0.0 2026-07-31 22:26:11 -07:00
Alex Xu b3e1d393b6 Manually label spreads button (#1412)
* initial commit

* initial commit 2

* json it

* UI labels

* basics

* add todo

* options to getWorkFolder

* refactor options

* refactor UI

* cleanup

* pdf fixed size

* remove comment

* finalized json creation

* fix help text

* finalized

* remove print

* add youtube
2026-07-31 22:25:41 -07:00
Alex Xu b4c71b3b91 pass options to slugify (#1411) 2026-07-29 17:06:52 -07:00
Alex Xu 7e412f12ae pass options to getWorkFolder (#1410) 2026-07-29 16:41:34 -07:00
Nils Leo 2076775d94 Create the destination dir before flattening a single-subfolder archive (#1405) 2026-07-23 22:43:16 -07:00
Alex Xu 59df22b62d add sort order note to readme 2026-07-20 15:06:12 -07:00
Alex Xu ff5edc5ce5 fix: Destination path... already exists (#1404) 2026-07-20 14:21:42 -07:00
Alex Xu a52c0ce03a Merge pull request #1394 from ciromattia/axu2-patch-1
Clarify Kobo profile + EPUB -> KEPUB
2026-07-10 18:42:03 -07:00
Alex Xu 1c0506fe7d Clarify Kobo profile + EPUB -> KEPUB 2026-07-10 18:41:48 -07:00
Alex Xu 806c6232c3 bump to 10.4.0 2026-07-10 18:39:41 -07:00
Alex Xu 6f9f5067d8 Merge pull request #1384 from drshliapa/fix/912-preserve-comicinfo
add Keep ComicInfo.xml in CBZ output option
2026-07-10 18:38:05 -07:00
Alex Xu 11be0dffcc make it an option 2026-07-10 18:32:44 -07:00
Alex Xu 4c10569ab1 millisecond timestamp kindlegen log (#1393) 2026-07-10 17:47:34 -07:00
Alex Xu 2a5cd6718d new user first startup: default is Paperwhite 12th Gen, split and rotate, RTL (#1392) 2026-07-10 14:48:48 -07:00
Alex Xu fd1468904f stop webtoon unchecking automatically check customm autocontrast (#1391) 2026-07-10 14:26:41 -07:00
Alex Xu feec40ffc8 more detailed kindlegen warnings (#1390)
* show kindlegen warnings and errors

* show kindlegen warnings

* dump kindlegen logs to txt

* move warnings to end

* don't show webtoon cover warning

* fix typo

* add timestamp to kindlegen log dump

* double enter

* add TODO

* add more TODO

* fix comment

* final clean up
2026-07-10 13:57:20 -07:00
Marian Leontiev f6ae7661bc fix macOS app version showing 0.0.0 in Info.plist (#1385) 2026-07-09 15:25:42 -07:00
Alex Xu ca14a4c813 don't crash on empty nested folders (#1388) 2026-07-08 17:46:46 -07:00
Alex Xu 0c1071a358 fix Access is denied with read only input folder (2013 bug) (#1387)
* fix Access is denied on read only input folders

* minimize changes
2026-07-08 17:22:23 -07:00
Alex Xu 858251f509 show entire kindlegen error (#1386) 2026-07-07 10:02:26 -07:00
Marian Leontiev 3c66aa9a67 preserve ComicInfo.xml in CBZ output for Other profile 2026-07-05 21:07:26 +02:00
Aldijonas 077957e665 Rename 'cover.jpg' file to '##cover.jpg' (#1383) 2026-07-05 07:34:26 -07:00
Aldijonas c3f86e29c8 Rename cover.jpg to !cover.jpg in CBZ (#1381)
Change cover image filename from 'cover.jpg' to '!cover.jpg' in line 1907 for correct cover display when using CBZ file format
2026-07-02 16:41:37 -07:00
Nils Leo 72e1546c2c fix: enable unrar on all distros, not just Fedora (#1376)
* fix: enable unrar on all distros, not just Fedora

unrar was gated behind distro.id() == 'fedora', so on Debian/Ubuntu
the binary was installed but never invoked. Try unrar unconditionally
after 7z, with an optional rarfile Python fallback.

Fixes CBR extraction on non-Fedora Linux distributions.

* fix: gate unrar on Linux and drop unused distro dependency

Debian/Ubuntu's '7zip' package ships the upstream 7zz binary, which
excludes RAR support for licensing reasons, so 7z fails to read .cbr
(RAR) archives. unrar was installed but only invoked when
distro.id() == 'fedora', so on Debian it was never tried and extraction
failed.

Per maintainer feedback, gate unrar on platform.system() == 'Linux'
(not needed on Windows or macOS) and drop the rarfile PyPI fallback to
avoid a new dependency. Since the code no longer imports distro, also
remove it from the requirements files and setup.py.
2026-07-02 15:07:36 -07:00
sa-mi 544e866b51 fix crash in smart cover crop for landscape covers (#1379) 2026-07-01 21:30:08 -07:00
Alex Xu 363a36a764 reduce job list minimum height from 150 to 110 pixels (#1378) 2026-07-01 17:34:17 -07:00
Alex Xu bece5290a3 remove 2nd row of buttons (#1377) 2026-06-29 17:17:41 -07:00
Alex Xu cab3703217 bump to 10.3.0 2026-06-26 10:23:51 -07:00
Alex Xu 3723bf9d52 add light novel mode (#1361)
* add a novel mode where it only resizes images

* add GUI

* cleanup

* fix typo

* fully cooked

* remove print

* clean up

* cook

* cook

* handle LA case

* cleanup
2026-06-26 10:23:28 -07:00
Alex Xu 2a6d61530f epub input: fix fusion (#1375) 2026-06-26 10:23:09 -07:00
Alex Xu 5396f1f9c4 name output epub/cbz directly (#1374) 2026-06-24 21:22:49 -07:00
Alex Xu f1b58c83d6 Replace copyfile + remove with move (#1373)
* remove mobiPath toclean

* remove copyfile _comic.zip
2026-06-24 18:58:27 -07:00
Alex Xu 0f009755b1 store settings separately 2026-06-23 17:37:46 -07:00
Alex Xu ee5bd150e5 add force ebok option 2026-06-23 17:37:46 -07:00
Alex Xu a55c0ddb08 add epub language option (#1371) 2026-06-23 13:29:10 -07:00
Alex Xu 799961407e re-arrange options into 4 columns (#1370)
* re-arrange options into 4 columns

* put delete input at the top
2026-06-22 22:27:00 -07:00
Alex Xu bc28df1f53 add invert direction and vertical 4 panel options (#1369) 2026-06-22 17:31:12 -07:00
Alex Xu a8b2c055bf epub input: invert spread shift (#1368) 2026-06-22 17:17:22 -07:00
dependabot[bot]andpublic afa9c7e7e6 Update psutil requirement from >=5.9.5 to >=7.2.2 (#1358)
Updates the requirements on [psutil](https://github.com/giampaolo/psutil) to permit the latest version.
- [Changelog](https://github.com/giampaolo/psutil/blob/master/docs/changelog.rst)
- [Commits](https://github.com/giampaolo/psutil/compare/v5.9.5...v7.2.2)

---
updated-dependencies:
- dependency-name: psutil
  dependency-version: 7.2.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:31 -07:00
dependabot[bot]andpublic 1f1b9a37fa Update distro requirement from >=1.8.0 to >=1.9.0 (#1357)
Updates the requirements on [distro](https://github.com/python-distro/distro) to permit the latest version.
- [Release notes](https://github.com/python-distro/distro/releases)
- [Changelog](https://github.com/python-distro/distro/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python-distro/distro/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: distro
  dependency-version: 1.9.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 14:04:15 -07:00
dependabot[bot]andpublic c4d9512cbc Bump actions/checkout from 6 to 7 (#1367)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [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/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  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>
2026-06-22 14:02:58 -07:00
Alex Xu 89fc6437dd don't crash if rosetta is not available on macOS (#1366)
* don't crash if rosetta is not available on macOS

* make error a dialog

* add newlines

* fix format

* modify strings
2026-06-20 17:55:04 -07:00
Alex Xu 1e57da08a9 time kindlegen (#1365) 2026-06-18 16:34:52 -07:00
Alex Xu b4ef37dbb9 bump Windows 7 numpy (#1362) 2026-05-31 18:43:10 -07:00
39 changed files with 2244 additions and 1636 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
+2 -2
View File
@@ -25,9 +25,9 @@ jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: 3.11
cache: 'pip'
+3 -3
View File
@@ -30,9 +30,9 @@ jobs:
env:
MACOSX_DEPLOYMENT_TARGET: '14.0'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: 3.11
cache: 'pip'
@@ -71,7 +71,7 @@ jobs:
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 16
- run: npm install -g appdmg
+2 -2
View File
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- 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
@@ -43,7 +43,7 @@ jobs:
pip3 install --upgrade pip pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- uses: actions/setup-node@v6
- uses: actions/setup-node@v7
with:
node-version: 16
- run: npm install -g appdmg
+3 -3
View File
@@ -35,9 +35,9 @@ jobs:
command: build_c2p
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: 3.11
cache: 'pip'
@@ -58,7 +58,7 @@ jobs:
name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.2
uses: signpath/github-action-submit-signing-request@v2.3
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+3 -3
View File
@@ -27,9 +27,9 @@ jobs:
env:
WINDOWS_7: 1
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: 3.8
cache: 'pip'
@@ -51,7 +51,7 @@ jobs:
name: windows7-build
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.2
uses: signpath/github-action-submit-signing-request@v2.3
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
+71 -32
View File
@@ -1,4 +1,8 @@
<img src="header.jpg" alt="Header Image" width="400">
Most manga conversion methods for ereaders add margins. But KCC enables true fullscreen from top to bottom.
![header](images/header_margins720.jpeg)
(feat. Ya Boy Kongming! and Fire Force from a [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1) PDF source.)
# KCC
@@ -11,53 +15,77 @@
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux.
Supported input formats include JPG/PNG image files in folders, archives like CBZ/EPUB, or PDF.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, PDF, and folders of images.
The absolute highest quality source files are DRM-free PDFs from [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical.
The PDFs are often x12000 resolution or better, while even the 10" Kindle Scribe is only x2480.
KCC's main goal is maximum image quality at significantly smaller file size. For example, KCC
1) can compress a 600 MB manga volume from Humble Bundle to 100 MB.
This is mostly accomplished by downscaling to the native resolution of your specific device.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
2) fixes black levels to avoid gray/faded blacks found in many Kindle Store manga
![black](images/black720.jpeg)
3) fixes [the rainbow effect on Kaleido 3 color eink](https://www.youtube.com/watch?v=Dw2HTJCGMhw) without blur:
![rainbow](images/rainbow720.jpeg)
4) can [semi-automatically detect 2-page spreads](https://www.youtube.com/watch?v=kfIX67f7Aqk) from pre-split single pages:
![before](images/spreadbefore720.jpeg)
You can view combined spreads either rotated or not:
![full](images/full720.jpeg)
You can view the split halves either before or after combined spreads:
![half](images/half720.jpeg)
## Demo
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
Then all your usb loaded comic files will show up next to all your normal books in your ereader library!
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The final result looks incredible on my 10" Kindle Scribe: https://www.youtube.com/watch?v=2CIHW2N9Enc
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.
**WARNING**: Kindle Scribe 2025 MOBI support may have blank pages. Use PDF instead.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
The GUI looks like this, built in Qt6.
There are a lot of options, but most people only care about the ones checked below.
Hover over each option to see details in a tooltip.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
2) unneccessary margins at the bottom of the screen
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
![settings](images/settings.png)
The GUI looks like this, built in Qt6, with my most commonly used settings:
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
Simply drag and drop your files/folders into the KCC window,
adjust your settings (hover over each option to see details in a tooltip),
and hit convert to create ereader optimized files.
You can change the default output directory by holding `Shift` while clicking the convert button.
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac or OpenMTP.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
## Kindle Jailbreak for KOreader
Jailbreaking to use KOreader is optional. KCC works just fine with the native mobi manga viewer.
But if you jailbreak your Kindle, you'll unlock additional features like:
1) compatibility with image formats better suited for manga like 4-bit PNG and WEBP. This enables better quality at **half the file size** compared to JPG which must be 8-bit.
This is visually lossless since eink screens are 4-bit.
2) no dependency on kindlegen, which can mean **twice as fast conversions** since you can convert to CBZ directly.
3) You can dither by partially checking the JPG/PNG box and can crop in KCC instead of KOreader. This may increase page turn speed and battery.
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
@@ -123,6 +151,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
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 or Kindle Scribe 2025.
- Where is KEPUB option?
- Choosing a Kobo profile and EPUB output will output KEPUB.
- 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
- Kindle panel view not working?
@@ -145,6 +175,8 @@ You'll need to install various tools to access important but optional features.
### KindleGen
(This is not needed for Koreader users on jailbroken Kindle. Just set output to CBZ instead.)
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
@@ -239,8 +271,12 @@ MAIN:
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE)
[Default=KV]
-m, --manga-style Manga style (right-to-left reading and splitting)
--lightnovel Only resize images and preserve original file structure.
--ebok Force EBOK tag instead of PDOC for MOBI
--invertdirection Invert page turn direction
-q, --hq Try to increase the quality of magnification
-2, --two-panel Display two not four panels in Panel View mode
--vertical4panel Show side panels first in virtual panel view
-w, --webtoon Webtoon processing mode
--ts TARGETSIZE, --targetsize TARGETSIZE
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
@@ -251,6 +287,7 @@ PROCESSING:
--pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
--wallpaper Crop images to to fill screen
-r SPLITTER, --splitter SPLITTER
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
-g GAMMA, --gamma GAMMA
@@ -289,8 +326,10 @@ OUTPUT SETTINGS:
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
--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]
--keepcomicinfo Keep any original ComicInfo.xml files [Default=0]
-a AUTHOR, --author AUTHOR
Author name [Default=KCC]
--language EPUB language [Default=en-US]
-f FORMAT, --format FORMAT
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'
+919 -870
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

View File

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

+61
View File
@@ -0,0 +1,61 @@
# -*- mode: python ; coding: utf-8 -*-
# macOS GUI build, used by setup.py build_binary
import sys
sys.path.insert(0, SPECPATH)
from kindlecomicconverter import __version__
a = Analysis(
['kcc.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='Kindle Comic Converter',
debug=False,
bootloader_ignore_signals=False,
strip=True,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['icons/comic2ebook.icns'],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=True,
upx=True,
upx_exclude=[],
name='Kindle Comic Converter',
)
app = BUNDLE(
coll,
name='Kindle Comic Converter.app',
icon='icons/comic2ebook.icns',
bundle_identifier=None,
info_plist={
'CFBundleShortVersionString': __version__,
'CFBundleVersion': __version__,
},
)
+271 -137
View File
@@ -19,10 +19,11 @@
from datetime import datetime, timezone
import itertools
import json
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView)
from PySide6.QtGui import (QColor, QIcon, QImage, QKeyEvent, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QDialogButtonBox, QHBoxLayout, QLabel, QListWidgetItem, QMainWindow, QSizePolicy, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView, QWidget)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -38,10 +39,15 @@ from xml.sax.saxutils import escape
from psutil import Process
from copy import copy
from packaging.version import Version
from tempfile import gettempdir
from tempfile import gettempdir, mkdtemp
from PIL import Image
from PIL.Image import Dither
from .KCC_spread_label import LabelSpreadsDialog
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, TAR, available_archive_tools
from .comic2ebook import OS_SORT_KEY, flattenTree, getWorkFolder, removeNonImages, sanitizeTree
from . import __version__
from . import comic2ebook
from . import metadata
@@ -121,6 +127,8 @@ class Icons:
self.MOBIFormat.addPixmap(QPixmap(":/Formats/icons/MOBI.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.CBZFormat = QIcon()
self.CBZFormat.addPixmap(QPixmap(":/Formats/icons/CBZ.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.FOLDERFormat = QIcon()
self.FOLDERFormat.addPixmap(QPixmap(":/Other/icons/folder_new.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon()
@@ -234,6 +242,140 @@ class ProgressThread(QThread):
self.running = False
def get_options():
parser = comic2ebook.makeParser()
options = parser.parse_args()
options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
options.format = gui_current_format
if GUI.mangaBox.isChecked():
options.righttoleft = True
if GUI.lightnovelBox.isChecked():
options.lightnovel = True
if GUI.wallpaperBox.isChecked():
options.wallpaper = True
if GUI.ebokBox.isChecked():
options.ebok = True
if GUI.invertDirectionBox.isChecked():
options.invertdirection = True
if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
options.splitter = 1
if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True
if GUI.vertical4PanelBox.isChecked():
options.vertical4panel = True
if GUI.webtoonBox.isChecked():
options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.stretch = True
elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
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:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.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:
options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
options.black_borders = True
if GUI.outputSplit.isChecked():
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.legacyExtractBox.isChecked():
options.legacyextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = True
if GUI.coverFillBox.isChecked():
options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
options.metadatatitle = 2
if GUI.keepComicInfoBox.isChecked():
options.keepcomicinfo = True
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked():
options.noquantize = True
if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2:
options.customwidth = str(GUI.widthBox.value())
options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '':
options.output = GUI.targetDirectory
if GUI.titleEdit.text():
options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.languageEdit.text():
options.language = str(GUI.languageEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
return options, gui_current_format
class WorkerThread(QThread):
def __init__(self):
QThread.__init__(self)
@@ -263,124 +405,9 @@ class WorkerThread(QThread):
def run(self):
MW.modeConvert.emit(0)
parser = comic2ebook.makeParser()
options = parser.parse_args()
argv = ''
currentJobs = []
options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
options.format = gui_current_format
if GUI.mangaBox.isChecked():
options.righttoleft = True
if GUI.rotateBox.checkState() == Qt.CheckState.PartiallyChecked:
options.splitter = 2
elif GUI.rotateBox.checkState() == Qt.CheckState.Checked:
options.splitter = 1
if GUI.qualityBox.checkState() == Qt.CheckState.PartiallyChecked:
options.autoscale = True
elif GUI.qualityBox.checkState() == Qt.CheckState.Checked:
options.hq = True
if GUI.webtoonBox.isChecked():
options.webtoon = True
if GUI.upscaleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.stretch = True
elif GUI.upscaleBox.checkState() == Qt.CheckState.Checked:
options.upscale = True
if GUI.gammaBox.isChecked() and float(GUI.gammaValue) > 0.09:
options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked():
options.autolevel = True
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:
options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.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:
options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
options.black_borders = True
if GUI.outputSplit.isChecked():
options.batchsplit = 2
if GUI.colorBox.isChecked():
options.forcecolor = True
if GUI.eraseRainbowBox.isChecked():
options.eraserainbow = True
if GUI.maximizeStrips.isChecked():
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.legacyExtractBox.isChecked():
options.legacyextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = True
if GUI.coverFillBox.isChecked():
options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
options.metadatatitle = 2
if GUI.deleteBox.isChecked():
options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked():
options.spreadshift = True
if GUI.onePageLandscapeBox.isChecked():
options.onepagelandscape = True
if GUI.fileFusionBox.isChecked():
options.filefusion = True
else:
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked():
options.noquantize = True
if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2:
options.customwidth = str(GUI.widthBox.value())
options.customheight = str(GUI.heightBox.value())
if GUI.targetDirectory != '':
options.output = GUI.targetDirectory
if GUI.titleEdit.text():
options.title = str(GUI.titleEdit.text())
if GUI.authorEdit.text():
options.author = str(GUI.authorEdit.text())
if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value())
options, gui_current_format = get_options()
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
@@ -419,6 +446,9 @@ class WorkerThread(QThread):
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files'
elif gui_current_format == 'FOLDER':
MW.addMessage.emit('Creating folders', 'info', False)
GUI.progress.content = 'Creating folders'
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files', 'info', False)
GUI.progress.content = 'Creating PDF files'
@@ -464,11 +494,13 @@ class WorkerThread(QThread):
GUI.progress.content = ''
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
elif gui_current_format == 'FOLDER':
MW.addMessage.emit('Creating folders... <b>Done!</b>', 'info', True)
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format:
if 'MOBI' in gui_current_format and not options.lightnovel:
MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
MW.progressBarTick.emit('tick')
@@ -509,7 +541,6 @@ class WorkerThread(QThread):
for item in outputPath:
GUI.progress.content = ''
mobiPath = item.replace('.epub', '.mobi')
os.remove(mobiPath + '_toclean')
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath):
try:
move(mobiPath, GUI.targetDirectory)
@@ -530,8 +561,6 @@ class WorkerThread(QThread):
mobiPath = item.replace('.epub', '.mobi')
if os.path.exists(mobiPath):
os.remove(mobiPath)
if os.path.exists(mobiPath + '_toclean'):
os.remove(mobiPath + '_toclean')
MW.addMessage.emit('Failed to process MOBI file!', 'error', False)
MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical')
else:
@@ -551,8 +580,6 @@ class WorkerThread(QThread):
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',
False)
if self.kindlegenErrorCode[0] == 3221226505:
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
else:
for item in outputPath:
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
@@ -652,6 +679,87 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom()
def labelSpreadsStart(self):
low_quality_preview = False
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
low_quality_preview = True
currentJobs = []
# TODO: make this a function since it's copy pasted
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text()))
for job in currentJobs:
images = []
spreads = []
options, _ = get_options()
options.profileData = [(600, 800), (600,800)]
path = getWorkFolder(job, options)
removeNonImages(path)
sanitizeTree(path, options)
flattenTree(path)
if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(job))
else:
workdir = mkdtemp('', 'KCC-')
for root, _, files in os.walk(path):
files.sort(key=OS_SORT_KEY)
start_index = 0
if job.endswith('.pdf') or job.endswith('.epub'):
start_index = 1
if options.spreadshift:
start_index = 0 if start_index == 1 else 1
for i in range(start_index, len(files), 2):
if i == len(files) - 1:
continue
# TODO: with statements
# TODO: ignore 1% of top and bottom too?
im1 = Image.open(os.path.join(root, files[i]))
im2 = Image.open(os.path.join(root, files[i+1]))
if not options.righttoleft:
im1, im2 = im2, im1
size1 = im1.size
size2 = im2.size
preview_crop1 = im1.crop((0, 0, 0.2*size1[0], size1[1]))
if low_quality_preview:
preview_crop1 = preview_crop1.convert('1', dither=Dither.NONE)
calculation_crop1 = im1.crop((0.01*size1[0], 0, 0.04*size1[0], size1[1])).convert('1', dither=Dither.NONE)
preview_crop2 = im2.crop((0.8*size2[0], 0, size2[0], size2[1]))
if low_quality_preview:
preview_crop2 = preview_crop2.convert('1', dither=Dither.NONE)
calculation_crop2 = im2.crop((0.96*size2[0], 0, .99* size2[0], size2[1])).convert('1', dither=Dither.NONE)
# dst = Image.new('1', (im1.width + im2.width, im1.height))
# dst.paste(im2, (0, 0))
# dst.paste(im1, (im1.width, 0))
hist1 = calculation_crop1.histogram()
hist2 = calculation_crop2.histogram()
# TODO: small percentage instead of zero
if hist1[0] == 0 or hist1[-1] == 0 or hist2[0] == 0 or hist2[-1] == 0:
continue
preview_mode = '1' if low_quality_preview else 'RGB'
dst = Image.new(preview_mode, (preview_crop1.width + preview_crop2.width, preview_crop1.height))
dst.paste(preview_crop2, (0, 0))
dst.paste(preview_crop1, (preview_crop1.width, 0))
dst.save(os.path.join(workdir, f'label-{i:04}.png'))
images.append(os.path.join(workdir, f'label-{i:04}.png'))
if images:
dlg = LabelSpreadsDialog(APP.primaryScreen().availableGeometry().height(), images, spreads)
dlg.setWindowTitle(job)
if dlg.exec() == 1:
with open(job+'.json', "w") as fp:
# TODO: not very clean to grab index from filename
spreads = [int(filename[6:10]) for filename in spreads]
json.dump({'spreads': sorted(set(spreads))} , fp)
rmtree(path, True)
rmtree(workdir, True)
def selectFileMetaEditor(self, sname):
files = []
@@ -743,7 +851,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
else:
status = True
GUI.editorButton.setEnabled(status)
GUI.wikiButton.setEnabled(status)
GUI.deviceBox.setEnabled(status)
GUI.defaultOutputFolderButton.setEnabled(status)
GUI.clearButton.setEnabled(status)
@@ -832,7 +939,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.interPanelCropBox.setEnabled(True)
GUI.autoLevelBox.setEnabled(True)
GUI.autocontrastBox.setEnabled(True)
GUI.autocontrastBox.setChecked(True)
def togglequalityBox(self, value):
@@ -865,6 +971,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.noQuantizeBox.setEnabled(False)
GUI.forcePngRgbBox.setEnabled(False)
def toggleWallpaperBox(self, value):
if value == 2:
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
GUI.rotateBox.setChecked(True)
GUI.noRotateBox.setChecked(True)
GUI.upscaleBox.setChecked(True)
self.changeFormat(3)
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
@@ -964,9 +1078,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.upscaleBox.setChecked(True)
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'FOLDER', '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')
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
else:
GUI.borderBox.setCheckState(Qt.CheckState.Unchecked)
@@ -1080,8 +1195,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('startNumber', self.startNumber + 1)
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'lightnovelBox': GUI.lightnovelBox.checkState(),
'wallpaperBox': GUI.wallpaperBox.checkState(),
'ebokBox': GUI.ebokBox.checkState(),
'invertDirectionBox': GUI.invertDirectionBox.checkState(),
'languageEdit': GUI.languageEdit.text(),
'rotateBox': GUI.rotateBox.checkState(),
'qualityBox': GUI.qualityBox.checkState(),
'vertical4PanelBox': GUI.vertical4PanelBox.checkState(),
'gammaBox': GUI.gammaBox.checkState(),
'autoLevelBox': GUI.autoLevelBox.checkState(),
'autocontrastBox': GUI.autocontrastBox.checkState(),
@@ -1101,6 +1222,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'keepComicInfoBox': GUI.keepComicInfoBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(),
@@ -1189,6 +1311,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.kindleGen = False
if startup:
self.display_kindlegen_missing()
except OSError as e:
self.kindleGen = False
if startup:
error = f"kindlegen: {e.strerror}\n\n Re-install Rosetta/Kindle Previewer/other Intel app?\n\nPlease email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help"
self.showDialog(error, 'error')
def __init__(self, kccapp, kccwindow):
global APP, MW, GUI
@@ -1198,17 +1325,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW)
self.editor = KCCGUI_MetaEditor()
self.icons = Icons()
self.settings = QSettings('ciromattia', 'kcc9')
self.settings = QSettings('ciromattia', 'kcc10')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
if not os.path.exists(self.defaultOutputFolder):
self.defaultOutputFolder = ''
self.lastDevice = self.settings.value('lastDevice', 0, type=int)
# default is Kindle Paperwhite 12th Gen
self.lastDevice = self.settings.value('lastDevice', 3, type=int)
self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int)
self.windowSize = self.settings.value('windowSize', '0x0', type=str)
default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}
default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100, 'rotateBox': 1, 'mangaBox': 2}
try:
self.options = self.settings.value('options', default_options)
except Exception:
@@ -1235,7 +1365,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if self.windowSize == '0x0':
MW.resize(500, 500)
elif sys.platform.startswith('darwin'):
for element in ['editorButton', 'wikiButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
for element in ['editorButton', 'defaultOutputFolderButton', 'clearButton', 'fileButton', 'deviceBox',
'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
@@ -1248,6 +1378,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"Folder of images": {'icon': 'FOLDER', 'format': 'FOLDER'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
"KFX (Send to Kindle EPUB)": {'icon': 'KFX', 'format': 'KFX'},
@@ -1416,8 +1547,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
link_dict = {
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'YOUTUBE': "https://youtu.be/IR2Fhcm9658?si=Z-2zzLaUFjmaEbrj",
'COMMISSIONS': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'WIKI': "https://github.com/ciromattia/kcc/wiki",
'YOUTUBE': "https://www.youtube.com/@eink-dude",
'TUTORIAL': "https://youtu.be/QQ6zJcMF2Iw?si=80rfm6DU6OUJdFqa",
'EMAIL': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
'FORUM': "http://www.mobileread.com/forums/showthread.php?t=207461",
'DISCORD': "https://discord.com/invite/qj7wpnUHav",
@@ -1449,12 +1582,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.fileButton.clicked.connect(self.selectFile)
GUI.directoryButton.clicked.connect(self.selectDir)
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble)
GUI.youtubeButton.clicked.connect(self.openYouTube)
GUI.discordButton.clicked.connect(self.openDiscord)
GUI.convertButton.clicked.connect(self.convertStart)
GUI.labelSpreadsButton.clicked.connect(self.labelSpreadsStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
@@ -1463,6 +1594,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
GUI.wallpaperBox.stateChanged.connect(self.toggleWallpaperBox)
GUI.chunkSizeCheckBox.stateChanged.connect(self.togglechunkSizeCheckBox)
GUI.deviceBox.activated.connect(self.changeDevice)
GUI.formatBox.activated.connect(self.changeFormat)
@@ -1512,6 +1644,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.widthBox.setValue(int(self.options[option]))
elif str(option) == "heightBox":
GUI.heightBox.setValue(int(self.options[option]))
elif str(option) == "languageEdit":
GUI.languageEdit.setText(str(self.options[option]))
elif str(option) == "gammaSlider":
if GUI.gammaSlider.isEnabled():
GUI.gammaSlider.setValue(int(self.options[option]))
+92
View File
@@ -0,0 +1,92 @@
import os
from PySide6.QtCore import (Qt)
from PySide6.QtGui import (QKeyEvent, QPixmap)
from PySide6.QtWidgets import (QHBoxLayout, QLabel, QDialog)
class LabelSpreadsDialog(QDialog):
def __init__(self, available_height, images, spreads):
super().__init__()
self.index = 0
self.images = images
self.spreads = spreads
self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
self.setWindowTitle("TODO: Filename goes here")
# self.setGeometry(APP.primaryScreen().availableGeometry())
# self.setMaximumSize(APP.primaryScreen().availableSize())
self.available_height = available_height
layout = QHBoxLayout()
self.setLayout(layout)
label = QLabel()
label2 = QLabel()
self.label = label
self.label2 = label2
layout.addWidget(label)
layout.addWidget(label2)
label2.setText("not a spread")
help_text = [
"Use arrows to change index.",
"Use space bar to confirm spreads.",
"Use spread shift option to offset by 1.",
"Use enter key to confirm all spreads."
"Close window to cancel."
]
buttonLabel = QLabel('\n'.join(help_text))
layout.addWidget(buttonLabel)
# print(label.size())
# print(label.maximumSize())
# l, t, r, b = layout.getContentsMargins()
#label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
label.setPixmap(pixmap)
#label.setScaledContents(True)
#label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# label2.setPixmap(pixmap2)
#label2.setScaledContents(True)
#self.resize(pixmap2.width(), pixmap2.height())
def keyReleaseEvent(self, event):
# t = 20
# b = 20
if isinstance(event, QKeyEvent):
if event.key() == Qt.Key.Key_Left:
self.index = max(0, self.index - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Right:
self.index = min(self.index + 1, len(self.images) - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Space:
if self.label2.text() == "not a spread":
self.spreads.append(os.path.basename(self.images[self.index]))
self.label2.setText('spread')
else:
self.spreads.remove(os.path.basename(self.images[self.index]))
self.label2.setText('not a spread')
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
self.accept()
else:
super().keyReleaseEvent(event)
else:
super().keyReleaseEvent(event)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.2.0'
__version__ = '11.1.0'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'
+244 -57
View File
@@ -19,6 +19,8 @@
#
from collections import Counter
from datetime import datetime
import json
import os
import pathlib
import re
@@ -34,12 +36,12 @@ from stat import S_IWRITE, S_IREAD, S_IEXEC
from typing import List
from zipfile import ZipFile, ZIP_STORED
from tempfile import mkdtemp, gettempdir
from shutil import move, copytree, rmtree, copyfile
from shutil import move, copytree, rmtree
from multiprocessing import Pool, cpu_count
from uuid import uuid4
from natsort import os_sort_keygen, os_sorted
from slugify import slugify as slugify_ext
from PIL import Image, ImageFile
from PIL import Image, ImageFile, ImageOps
from pathlib import Path
from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import virtual_memory, disk_usage
@@ -226,7 +228,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
f = open(ncxfile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
f"<ncx version=\"2005-1\" xml:lang=\"{options.language}\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
"<head>\n",
"<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
"<meta name=\"dtb:depth\" content=\"1\"/>\n",
@@ -292,10 +294,22 @@ def buildNAV(dstdir, title, chapters, chapternames):
def buildOPF(dstdir, title, filelist, originalpath, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
deviceres = options.profileData[1]
if options.righttoleft:
writingmode = "horizontal-rl"
if options.vertical4panel:
writingmode = "vertical"
else:
writingmode = "horizontal-lr"
writingmode = "horizontal"
if options.invertdirection:
if options.righttoleft:
writingmode += "-lr"
else:
writingmode += "-rl"
else:
if options.righttoleft:
writingmode += "-rl"
else:
writingmode += "-lr"
f = open(opffile, "w", encoding='UTF-8')
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
"<package version=\"3.0\" unique-identifier=\"BookID\" ",
@@ -303,7 +317,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
"<dc:title>", hescape(title), "</dc:title>\n",
"<dc:language>en-US</dc:language>\n",
f"<dc:language>{options.language}</dc:language>\n",
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
if len(options.summary) > 0:
@@ -394,13 +408,22 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
else:
return ""
if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
pageside = "right"
if options.invertdirection:
if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
else:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
pageside = "right"
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
if originalpath.lower().endswith('.pdf'):
if options.righttoleft:
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
pageside = "right"
else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left"
if originalpath.lower().endswith('.pdf') or originalpath.lower().endswith('.epub'):
if pageside == "right":
pageside = "left"
else:
@@ -850,7 +873,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height, pdfwidth):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
@@ -869,8 +892,15 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
cpu = cpu_count()
if not render:
print('PDF input can also use legacy extract option if you have any problems')
if GUI:
GUI.addMessage.emit('PDF input can also use legacy extract option if you have any problems'
, 'info', False)
GUI.addMessage.emit('', '', False)
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
vectors = [(i, cpu, filename, output_dir, target_width, target_height, pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
@@ -884,7 +914,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
def getWorkFolder(afile, workdir=None):
def getWorkFolder(afile, options, workdir=None):
if not workdir:
if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
@@ -933,7 +963,7 @@ def getWorkFolder(afile, workdir=None):
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
target_width *= 1.25
try:
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height, options.pdfwidth)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -944,6 +974,8 @@ def getWorkFolder(afile, workdir=None):
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(fullPath)
if options.lightnovel:
return workdir
sanitizePermissions(path)
tdir = os.listdir(fullPath)
@@ -955,9 +987,16 @@ def getWorkFolder(afile, workdir=None):
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
if options.tempdir:
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
else:
workdir2 = mkdtemp('', 'KCC-')
fullPath2 = os.path.join(workdir2, 'OEBPS', 'Images')
os.makedirs(fullPath2, exist_ok=True)
for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath)
os.rmdir(os.path.join(fullPath, tdir[0]))
move(os.path.join(fullPath, tdir[0], file), fullPath2)
rmtree(workdir, True)
return workdir2
if options.legacyextract:
return workdir
@@ -1026,6 +1065,8 @@ def getWorkFolder(afile, workdir=None):
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
if options.folder_output:
ext = ''
source_path = Path(srcpath)
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
@@ -1037,7 +1078,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
ext = '.kepub.epub'
if wantedname is not None:
wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext):
if not options.folder_output and wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif wanted_ext == '.mobi' and ext == '.epub':
filename = os.path.abspath(wanted_root + ext)
@@ -1061,10 +1102,10 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
filename = source_path.with_name(name)
else:
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
if os.path.isfile(filename):
if os.path.exists(filename):
counter = 0
basename = os.path.splitext(filename)[0]
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
while os.path.exists(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
elif options.format == 'MOBI' and ext == '.epub':
@@ -1080,6 +1121,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
def getMetadata(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml')
options.comicinfo_chapters = []
options.comicinfo_xml = None
options.summary = ''
titleSuffix = ''
options.volume = ''
@@ -1136,6 +1178,10 @@ def getMetadata(path, originalpath):
options.summary = xml.data['Summary']
if xml.data['Series']:
options.series = xml.data['Series']
# ComicInfo.xml in output may break readers like the Kobo native CBZ reader
if options.keepcomicinfo and options.format == 'CBZ':
with open(xmlPath, 'rb') as f:
options.comicinfo_xml = f.read()
os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'):
@@ -1178,14 +1224,22 @@ def removeNonImages(filetree):
os.remove(os.path.join(root, name))
# remove empty nested folders
for root, dirs, files in os.walk(filetree, False):
if not files and not dirs:
if not os.listdir(root):
os.rmdir(root)
if not os.listdir(Path(filetree).parent):
raise UserWarning('No images detected, nested archives are not supported.')
warning = [
'No images detected.',
'',
'Possible causes:',
'',
'1) Incompatible image file extension like .jxl. Convert to .png first outside of KCC.',
'2) Nested archive: Either extract the nested archive outside of KCC or use File Fusion option.',
]
raise RuntimeError('\n'.join(warning))
def sanitizeTree(filetree, prefix='kcc'):
def sanitizeTree(filetree, options, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1210,7 +1264,7 @@ def sanitizeTree(filetree, prefix='kcc'):
dirs.sort(key=OS_SORT_KEY)
for i, name in enumerate(dirs):
tmpName = name
slugified = slugify(name, is_natural_sorted)
slugified = slugify(name, options, is_natural_sorted)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
slugified += "A"
chapterNames[slugified] = tmpName
@@ -1231,6 +1285,7 @@ def flattenTree(filetree):
def sanitizePermissions(filetree):
os.chmod(filetree, S_IWRITE | S_IREAD | S_IEXEC)
for root, dirs, files in os.walk(filetree, False):
for name in files:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
@@ -1372,7 +1427,7 @@ def createNewTome(parent):
return tomePath, tomePathRoot
def slugify(value, is_natural_sorted):
def slugify(value, options, is_natural_sorted):
if options.format == 'CBZ' and is_natural_sorted:
return value
if options.format != 'CBZ':
@@ -1384,8 +1439,10 @@ def slugify(value, is_natural_sorted):
return value
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
if options.folder_output:
move(basedir, zipfilename)
return zipfilename
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools():
if isepub:
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
@@ -1428,10 +1485,20 @@ def makeParser():
" [Default=KV]")
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (right-to-left reading and splitting)")
main_options.add_argument("--lightnovel", action="store_true", dest="lightnovel", default=False,
help="Only resize images and preserve original file structure.")
main_options.add_argument("--wallpaper", action="store_true", dest="wallpaper", default=False,
help="Crop to fill screen.")
main_options.add_argument("--ebok", action="store_true", dest="ebok", default=False,
help="Force EBOK tag instead of PDOC for MOBI")
main_options.add_argument("--invertdirection", action="store_true", dest="invertdirection", default=False,
help="Invert page turn direction")
main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
help="Try to increase the quality of magnification")
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
help="Display two not four panels in Panel View mode")
main_options.add_argument("--vertical4panel", action="store_true", dest="vertical4panel", default=False,
help="Display side panels first in virtual panel view")
main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
help="Webtoon processing mode"),
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
@@ -1445,8 +1512,12 @@ def makeParser():
output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0,
help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema "
"2: Use Title only")
output_options.add_argument("--keepcomicinfo", type=int, dest="keepcomicinfo", default=0,
help="Keep any original ComicInfo.xml files")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]")
output_options.add_argument("--language", action="store", dest="language", default="en-US",
help="EPUB language [Default=en-US]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]")
@@ -1554,6 +1625,9 @@ def checkOptions(options):
else:
options.isKobo = True
if options.lightnovel:
options.noKepub = True
if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
@@ -1579,6 +1653,12 @@ def checkOptions(options):
options.keep_epub = True
options.format = 'MOBI'
options.kfx = False
options.skip_zip = False
options.folder_output = False
if options.format == 'FOLDER':
options.format = 'CBZ'
options.skip_zip = True
options.folder_output = True
if options.format == 'Auto':
if options.profile in ['KDX']:
options.format = 'CBZ'
@@ -1673,6 +1753,11 @@ def checkTools(source):
except (FileNotFoundError, CalledProcessError):
print('ERROR: KindleGen is missing!')
sys.exit(1)
except OSError as e:
print(f"kindlegen: {e.strerror}")
print('Re-install Rosetta/Kindle Previewer/other Intel app?')
print('Please email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help')
sys.exit(1)
def checkPre(source='KCC-'):
@@ -1718,8 +1803,10 @@ def makeFusion(sources: List[str]):
else:
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
getWorkFolder(source, str(targetpath))
sanitizeTree(targetpath, prefix='fusion')
path = getWorkFolder(source, options, str(targetpath))
if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
sanitizeTree(targetpath, options, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath)
@@ -1742,15 +1829,90 @@ def makeBook(source, qtgui=None, job_progress=''):
if not options.filefusion:
checkPre('LLL-')
print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source)
path = getWorkFolder(source, options)
print(f"{job_progress}Checking images...")
_, ext = os.path.splitext(source)
if options.lightnovel:
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for file in files:
_, ext = os.path.splitext(file)
if ext.lower() in ('.jpg', '.jpeg', '.png', '.webp', '.gif'):
with Image.open(os.path.join(root, file)) as img:
# TODO: detect BW images saved as RGB
if not options.forcecolor:
if img.mode == 'RGB':
img = img.convert('L')
elif img.mode == 'RGBA':
img = img.convert('LA')
x, y = image.ProfileData.Profiles[options.profile][1]
if options.iskindle:
x = min(x, 1920)
y = min(y, 1920)
if img.size[0] > x or img.size[1] > y:
img = ImageOps.contain(img, (x, y))
img.save(os.path.join(root, file), quality=options.jpegquality)
if ext != '.epub':
ext = '.cbz'
output_file = getOutputFilename(source, options.output, ext, '')
makeZIP(output_file, os.path.join(path, 'OEBPS', 'Images'), job_progress)
rmtree(path, True)
return [output_file]
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'), options)
if os.path.exists(source+'.json'):
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
chapterNames = {}
cover_path = None
with open(source+'.json') as f:
data = json.load(f)
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
sorted_files = os_sorted(files)
for i in range(len(sorted_files)):
if not cover_path:
cover_path = os.path.join(root, sorted_files[i])
if i in data['spreads']:
im1 = Image.open(os.path.join(root, sorted_files[i]))
im2 = Image.open(os.path.join(root, sorted_files[i+1]))
if not options.righttoleft:
im1, im2 = im2, im1
dst = Image.new('RGB', (im1.width + im2.width, im1.height))
dst.paste(im2, (0, 0))
dst.paste(im1, (im1.width, 0))
base, _ = os.path.splitext(os.path.basename(sorted_files[i]))
dst.save(os.path.join(path, 'OEBPS', 'Images', f'{base}-merged.png'))
os.remove(os.path.join(root, sorted_files[i]))
os.remove(os.path.join(root, sorted_files[i+1]))
if not os.path.exists(cover_path):
cover_path = os.path.join(root, f'{base}-merged.png')
if options.filefusion:
# Strip the fusion_0001_ sort prefix from makeFusion if present
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
source_path = Path(source)
options.customcover = False
if source_path.parent.joinpath('Covers').is_dir():
series = os_sorted(filter(lambda s: s.endswith(ext) and '_kcc' not in s, os.listdir(source_path.parent)))
source_index = series.index(os.path.basename(source))
covers = os.listdir(source_path.parent.joinpath('Covers'))
filtered_covers = []
for cover in covers:
_, cover_ext = getImageFileName(cover)
if cover_ext in IMAGE_TYPES:
filtered_covers.append(cover)
sorted_covers = os_sorted(filtered_covers)
try:
cover_path = source_path.parent.joinpath('Covers', sorted_covers[source_index])
options.customcover = True
except IndexError:
pass
cover = None
if not options.webtoon:
cover = image.Cover(cover_path, options)
@@ -1832,15 +1994,18 @@ def makeBook(source, qtgui=None, job_progress=''):
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if cover and cover.smartcover:
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
if cover and (cover.smartcover or options.customcover):
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', '##cover.jpg'), tomeNumber, len(tomes))
if options.comicinfo_xml:
with open(os.path.join(tome, 'OEBPS', 'Images', 'ComicInfo.xml'), 'wb') as xmlOutput:
xmlOutput.write(options.comicinfo_xml)
makeZIP(filepath[-1], 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)
if cover and cover.smartcover:
if cover and (cover.smartcover or options.customcover):
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
# use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
@@ -1853,19 +2018,11 @@ def makeBook(source, qtgui=None, job_progress=''):
else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, job_progress, True)
# Copy files to final destination (PDF files are already saved directly)
if options.format != 'PDF':
copyfile(tome + '_comic.zip', filepath[-1])
try:
os.remove(tome + '_comic.zip')
except FileNotFoundError:
# newly temporary created file is not found. It might have been already deleted
pass
makeZIP(filepath[-1], tome, job_progress, True)
rmtree(tome, True)
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI':
if not GUI and options.format == 'MOBI' and not options.lightnovel:
print(f"{job_progress}Creating MOBI files...")
work = []
for i in filepath:
@@ -1884,8 +2041,6 @@ def makeBook(source, qtgui=None, job_progress=''):
if not output[0]:
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath
else:
os.remove(i.replace('.epub', '.mobi') + '_toclean')
if cover and k.path and k.coverSupport:
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
if options.delete:
@@ -1906,12 +2061,13 @@ def makeBook(source, qtgui=None, job_progress=''):
def makeMOBIFix(item, uuid):
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
if options.ebok:
is_pdoc = False
if not options.keep_epub:
os.remove(item)
mobiPath = item.replace('.epub', '.mobi')
move(mobiPath, mobiPath + '_toclean')
try:
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
dualmetafix.DualMobiMetaFix(mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
return [True]
except Exception as err:
return [False, format(err)]
@@ -1921,8 +2077,14 @@ def makeMOBIWorkerTick(output):
makeMOBIWorkerOutput.append(output)
if output[0] != 0:
makeMOBIWorkerPool.terminate()
for warning in output[3]:
print(warning)
if GUI:
GUI.progressBarTick.emit('tick')
if output[3]:
for warning in output[3]:
GUI.addMessage.emit(warning, 'warning', False)
GUI.addMessage.emit('', '', False)
if not GUI.conversionAlive:
makeMOBIWorkerPool.terminate()
@@ -1932,33 +2094,58 @@ def makeMOBIWorker(item):
kindlegenErrorCode = 0
kindlegenError = ''
try:
# TODO: This size check is incorrect, I think kindlegen increased the limit
if os.path.getsize(item) < 629145600:
start = perf_counter()
# TODO: should anything be done with the kindlegen output during successes?
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
end = perf_counter()
print(f"kindlegen: {end - start} sec")
else:
# ERROR: EPUB too big
kindlegenErrorCode = 23026
return [kindlegenErrorCode, kindlegenError, item]
return [kindlegenErrorCode, kindlegenError, item, []]
except CalledProcessError as err:
warnings = []
for line in err.stdout.splitlines():
# ERROR: Generic error
if "Error(" in line:
kindlegenErrorCode = 1
kindlegenError = line
kindlegenError = '\n\n'.join(warnings + [line, 'kindlegen logs dumped'])
try:
timestamp = datetime.now().isoformat(timespec='milliseconds').replace(':', '-').replace('.', '-')
with open(os.path.join(os.path.dirname(item), f'kindlegen-log-{timestamp}.txt'), 'w') as f:
f.write(err.stdout)
except Exception as e:
print(e)
# examples
# Warning(prcgen):W14019: Cover is too small
if "Warning(" in line:
if ":W14016: Cover not specified" in line and options.webtoon:
pass
else:
warnings.append(line)
# ERROR: EPUB too big
if ":E23026:" in line:
kindlegenErrorCode = 23026
if ":E23028:" in line:
kindlegenErrorCode = 23028
if kindlegenErrorCode > 0:
break
if ":I1036: Mobi file built successfully" in line:
return [0, '', item]
return [0, '', item, warnings]
if ":I1037: Mobi file built with WARNINGS!" in line:
return [0, '', item]
return [0, '', item, warnings]
# ERROR: KCC unknown generic error
if kindlegenErrorCode == 0:
kindlegenErrorCode = err.returncode
kindlegenError = err.stdout
return [kindlegenErrorCode, kindlegenError, item]
kindlegenErrorCode = -1
if err.returncode == 3221226505:
kindlegenError = f'Error {err.returncode}: Unknown Windows error. Possibly filepath too long?'
else:
kindlegenError = f'Error {err.returncode}'
return [kindlegenErrorCode, kindlegenError, item, warnings]
def makeMOBI(work, qtgui=None):
+3 -1
View File
@@ -62,7 +62,7 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072 * 8:
if targetHeight > 131072 * 4:
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))
y = 0
@@ -112,6 +112,8 @@ def splitImage(work):
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
widthImg, heightImg = imgOrg.size
if widthImg < 300:
raise UserWarning('PDF render failed < 300 px. Try legacy extract option instead.')
if heightImg > opt.height:
if opt.debug:
drawImg = Image.open(filePath).convert(mode='RGBA')
+6 -7
View File
@@ -22,7 +22,6 @@ from functools import cached_property, lru_cache
import os
from pathlib import Path
import platform
import distro
from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError
@@ -41,12 +40,12 @@ class ComicArchive:
self.dirname, self.basename = os.path.split(filepath)
@cached_property
def type(self):
def type(self):
extraction_commands = [
[SEVENZIP, 'l', '-y', '-p1', self.basename],
]
if distro.id() == 'fedora' or distro.like() == 'fedora':
if platform.system() == 'Linux':
extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.basename],
)
@@ -85,20 +84,20 @@ class ComicArchive:
extraction_commands.reverse()
if distro.id() == 'fedora' or distro.like() == 'fedora':
if platform.system() == 'Linux':
extraction_commands.append(
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
)
for cmd in extraction_commands:
try:
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
return targetdir
return targetdir
except FileNotFoundError:
missing.append(cmd[0])
except CalledProcessError:
pass
if missing:
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
else:
+1 -2
View File
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
class DualMobiMetaFix:
def __init__(self, infile, outfile, asin, is_pdoc):
def __init__(self, outfile, asin, is_pdoc):
cdetype = b'EBOK'
if is_pdoc:
cdetype = b'PDOC'
shutil.copyfile(infile, outfile)
f = open(outfile, "r+b")
self.datain = mmap.mmap(f.fileno(), 0)
self.datain_rec0 = readsection(self.datain, 0)
+6 -2
View File
@@ -513,7 +513,9 @@ class ComicPage:
self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self):
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
if self.opt.wallpaper:
pass
elif 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)
@@ -536,6 +538,8 @@ class ComicPage:
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
elif self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif self.opt.wallpaper:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
else: # if image bigger than device resolution or smaller with upscaling
@@ -648,7 +652,7 @@ class Cover:
if self.options.righttoleft:
self.image = self.image.crop((w * .36, 0, w, h))
else:
self.image = self.image.crop((w, 0, .64 * w, h))
self.image = self.image.crop((0, 0, .64 * w, h))
def save_to_folder(self, target, tomeid, len_tomes=0):
try:
+1 -2
View File
@@ -1,11 +1,10 @@
Pillow>=11.3.0
psutil>=5.9.5
psutil>=7.2.2
requests>=2.34.2
python-slugify>=8.0.4
packaging>=26.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
+1 -2
View File
@@ -1,11 +1,10 @@
PySide6==6.4.3
Pillow>=11.3.0
psutil>=5.9.5
psutil>=7.2.2
requests>=2.34.2
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy<2
PyMuPDF==1.25.5
+2 -3
View File
@@ -1,11 +1,10 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
psutil>=7.2.2
requests>=2.32.4
python-slugify>=8.0.4
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy==1.23.0
numpy==1.23.5
PyMuPDF>=1.16
+1 -2
View File
@@ -1,11 +1,10 @@
PySide6<6.10
Pillow>=11.3.0
psutil>=5.9.5
psutil>=7.2.2
requests>=2.34.2
python-slugify>=8.0.4,<9.0.0
packaging>=26.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4
PyMuPDF>=1.18.0
+1 -2
View File
@@ -38,7 +38,7 @@ class BuildBinaryCommand(setuptools.Command):
def run(self):
VERSION = __version__
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 -y kcc-macos.spec')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
if min_os.startswith('10.1'):
@@ -155,7 +155,6 @@ setuptools.setup(
'python-slugify>=1.2.1,<9.0.0',
'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0',
'distro>=1.8.0',
'numpy>=1.22.4',
'packaging>=23.2',
'PyMuPDF>=1.16.1',