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

Compare commits

...

133 Commits

Author SHA1 Message Date
Alex Xu
8d61a9e558 bump to 9.7.0 2026-04-12 11:31:27 -07:00
Alex Xu
8aaedf274d add youtube, discord, humble buttons (#1292)
* add youtube, discord, humble buttons

* restore margins of top buttons

* fix button height on windows

* don't bold messages
2026-04-12 11:30:18 -07:00
Alex Xu
5782a44e7b sign windows 7 version (#1291) 2026-04-12 09:17:23 -07:00
Alex Xu
e4c918f0f3 add webp output support (ignored for Kindle MOBI/EPUB and all PDF) (#1290)
* add webp output support (ignored for Kindle MOBI/EPUB)

* disable png extra optons by default

* pdf webp is not supported
2026-04-12 09:04:21 -07:00
Alex Xu
8f4072bfab disable extra png options when png is not selected (#1289) 2026-04-11 19:32:49 -07:00
Alex Xu
61f3097be5 smart cover crop is now an option default off (#1288) 2026-04-11 19:09:04 -07:00
Alex Xu
fa33ef8f89 revert 2x render, too expensive (#1287) 2026-04-11 18:06:03 -07:00
Alex Xu
232bac00a9 ZIP stored instead of deflate (#1283) 2026-04-06 21:03:17 -07:00
Alex Xu
d19a4754fa if cropping, render pdf to 2x (#1276) 2026-03-26 08:09:25 -07:00
Alex Xu
9a93cc4b17 Update __init__.py 2026-03-17 16:11:57 -07:00
Alex Xu
e0471b2dc9 fix autocrop in certain situations (#1274) 2026-03-17 16:11:40 -07:00
Alex Xu
a87eb318cf bump 9.6.1 2026-03-17 15:10:02 -07:00
Alex Xu
87987c9ebf fix humble bundle pdf png autocontrast (#1273) 2026-03-17 15:09:02 -07:00
Alex Xu
f5fe8d93b0 color images are always saved as JPG by default (#1272)
* use jpg for color images always

* add colorOutput variable

* fix typos

* remove dither

* add box

* clarify png

* remove debug code

* remove unneeded check
2026-03-17 13:41:41 -07:00
Alex Xu
249f823f01 add PNG legacy option (#1271) 2026-03-13 21:45:05 -07:00
Alex Xu
3a9d4f274d bump to 9.6.0 2026-03-13 16:29:50 -07:00
Alex Xu
b5de6fd39d add pdf width box (#1270) 2026-03-13 16:28:53 -07:00
Alex Xu
b4b9e41a0c add no quantize option (#1269) 2026-03-13 15:07:41 -07:00
Alex Xu
9b9181a715 Add rotate right option (#1268) 2026-03-13 14:15:14 -07:00
Alex Xu
472fdc97b5 PDF PNG half size, Kindle DX PNG CBZ fixed (#1267)
* PNG for PDF or Kindle DX CBZ is dithered to 16 colors for ~1/2 filesize reduction

* Kindle DX default no borders
2026-03-13 12:17:38 -07:00
Alex Xu
6fdfddd7d9 bump to 9.5.1 2026-03-10 14:00:54 -07:00
Alex Xu
34fb68ac65 downgrade to pyside 6.9 2026-03-10 13:50:59 -07:00
Alex Xu
b4d72cd581 remove setuptools and wheel (#1265) 2026-03-10 13:37:59 -07:00
Alex Xu
1dead9af8f add PDF 200 MB option (#1264) 2026-03-10 13:29:46 -07:00
Alex Xu
b42f05686e bump to 9.5.0 2026-03-08 17:37:47 -07:00
Alex Xu
adf48d24f9 clarify coverfill is not implemented for kindle scribe (#1255) 2026-02-22 11:49:27 -08:00
tom
723fa4c0b8 Add exact cover fit option for device-sized cover cropping (#1254)
* Add exact cover fit option for device-sized cover cropping

* Update README to move cover cropping from FAQ to USAGE

* rename to coverfill

* edit readme
2026-02-22 11:34:10 -08:00
Alex Xu
2632d18e2c Update README.md 2026-02-16 15:15:09 -08:00
Alex Xu
94e4937566 ensure mimetype is first when using 7zip (#1251) 2026-02-15 13:01:43 -08:00
Alex Xu
f7ce1cf271 add demo video 2026-02-14 23:43:34 -08:00
フィルターペーパー
ab93c03838 Preserve file fusion input order with prefix (#1242)
* Preserve file fusion input order with prefix

Prepend a sequence index to temporary directory names to ensure user-specified order is preserved.

* don't sort items in GUI

* Add prefix only if sorted order is different

* Simplified sort prefix that will be removed from chapter name

* Restore adding prefix only when sorted order differs

* simplify prefix code

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2026-02-09 21:34:11 -08:00
Alex Xu
541b1d876b save jpeg quality value (#1248) 2026-02-09 19:08:26 -08:00
Alex Xu
d189f9909d don't overwrite mobi output with same name (#1246) 2026-02-09 11:30:59 -08:00
Alex Xu
1dce4f8d2c support latest pyside6 6.10.2 (#1245) 2026-02-08 13:43:43 -08:00
Sébastien CHEMIN
58aab0cb65 Lower minimum chunk size to 50 MB, Remarkable chunk size default of 100 MB (#1240)
* accept smaller chunks size on gui

* add default target size for Remarkable to 95

* remove rc changes
2026-02-06 09:46:42 -08:00
Alex Xu
d2dc089c62 add workers crashed restart pc message (#1239) 2026-02-03 09:59:51 -08:00
Alex Xu
3660f2370f add kindlegen error to GUI (#1237) 2026-02-03 08:10:29 -08:00
Alex Xu
87c6e3a35e Update README.md 2026-02-02 09:15:00 -08:00
Alex Xu
981c556550 add new tutorial 2026-02-02 09:14:41 -08:00
Alex Xu
123d603cbd clarify mac can't be opened 2026-02-01 10:10:19 -08:00
Alex Xu
a344dd73bf add OS support to beginning 2026-01-28 10:44:22 -08:00
Alex Xu
095694e9cf use bsdtar on linux (#1234) 2026-01-27 09:03:01 -08:00
Alex Xu
4b4860b976 Bump to 9.4.3 2026-01-26 14:45:28 -08:00
Alex Xu
56e8e24176 fix release notes (#1231) 2026-01-26 14:44:36 -08:00
Alex Xu
b0f8f1c633 fix file fusion bugs (#1230) 2026-01-26 14:39:05 -08:00
Alex Xu
38acc3bf05 skip blanks on image based pdfs (#1228) 2026-01-26 09:56:44 -08:00
Alex Xu
fbd5980b9b add Kindle 1240x1860 profile (#1227) 2026-01-26 09:41:49 -08:00
Alex Xu
667d702b8a Kindle Scribe 2025 warning 2026-01-25 13:45:15 -08:00
Alex Xu
9a4143ce62 Add legacy pdf image extract option (#1225) 2026-01-25 13:41:43 -08:00
Alex Xu
f63387cae4 remove corrupt image checking (#1221)
* remove corrupt image checking

Removed image verification step before copying the image.

* Update image.py
2026-01-20 20:48:18 -08:00
Alex Xu
f5fd2bb7fe fix cropping divide by zero (#1220) 2026-01-20 20:47:56 -08:00
Alex Xu
4baca03214 Bump version to 9.4.2 2026-01-11 13:19:41 -08:00
Alex Xu
7de212dca3 move fixed resolution Kindle profiles down 2026-01-10 20:40:09 -08:00
Carlos Lázaro Costa
c99444b96a docs: Update Profiles (#1218)
* Update README Profiles

Align the README Profiles section with the profile definitions
in image.py.

- Add missing Kindle Scribe 3, Colorsoft and reMarkable profiles
- Update gamma values to default (from 1.8 to 1.0)

* Update profile name KS1860

Fix typo (1920 -> 1860)
2026-01-10 16:53:40 -08:00
Alex Xu
6d7a635c3d Fix Kindle Scribe 2025 resolutions (#1217) 2026-01-09 13:46:16 -08:00
Alex Xu
be86bcbf6a Add macOS 15 unsigned instructions 2026-01-08 20:20:10 -08:00
Alex Xu
5cbc07e65d increase webtoon max height (#1213) 2026-01-08 18:21:45 -08:00
Alex Xu
42d94d8202 increase max Windows path length from 180 to 220 (#1211) 2026-01-08 18:21:32 -08:00
フィルターペーパー
7897627c43 Preserve input file order using list instead of set (#1209)
* Preserve input file order using list instead of set

* Simplify list comprehension and fix append for sources

* Remove redundant list() conversion
2026-01-05 20:52:53 -08:00
Alex Xu
8e42fc1162 remove padding from output folder GUI (#1207) 2026-01-01 22:55:54 -08:00
Alex Xu
d6b0e43d70 Bump version to 9.4.1 2026-01-01 22:26:23 -08:00
Alex Xu
af189ed265 add Kindle 1920 profiles (7.4 Scribe behavior) (#1206) 2026-01-01 22:26:04 -08:00
Alex Xu
aa5f4991dd Bump version to 9.4.0 2025-12-29 10:52:34 -08:00
jaroslawjanas
f9064ef0e4 configurable jpg quality (#1205) 2025-12-29 10:37:56 -08:00
Alex Xu
e14abe1787 default jpg quality of 90 for scribe colorsoft (#1204) 2025-12-28 14:54:12 -08:00
Alex Xu
c58387f4f4 partial support for Kindle Scribe 2025 models (#1203)
* partial Kindle Scribe 2025 support

* make variables better

* remove quad
2025-12-27 17:18:05 -08:00
Alex Xu
9b63b7af2c Bump version to 9.3.8 2025-12-27 12:23:39 -08:00
tokyis
f74e108a3e Fixed resizing bug
caused by misplaced closing parenthesis.
2025-12-27 12:22:10 -08:00
Alex Xu
f088ad732e default MACOSX_DEPLOYMENT_TARGET 2025-12-23 13:19:40 -08:00
jaroslawjanas
8e5d57364d Remove environment.yml 2025-12-15 09:48:49 -08:00
dependabot[bot]
b767d5dc2c Bump actions/upload-artifact from 5 to 6 (#1196)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  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-12-15 09:48:49 -08:00
Alex Xu
7228055bca reduce file operations in webtoon and file fusion (#1191)
* reduce file operations in webtoon

* reduce file operations of file fusion

* fix file fusion failed to prepare

* close webtoon image before remove

* use temp directory
2025-12-14 23:44:30 -08:00
Alex Xu
8c57fbf318 fix add folder button sizePolicy 2025-12-14 18:59:28 -08:00
Alex Xu
7e94861fa1 input folder multiselect (#1195) 2025-12-14 16:13:24 -08:00
Alex Xu
9992ca4d26 fix mac legacy build naming 2025-12-10 22:00:56 -08:00
Alex Xu
f47d1427f0 Update README.md
Clarified description of Kindle Comic Converter's capabilities.
2025-12-09 23:22:31 -08:00
Alex Xu
ce8998375c macOS 14 minimum for non legacy 2025-12-08 22:27:40 -08:00
Alex Xu
8870898a87 macOS 14 minimum for non legacy builds (#1189) 2025-12-08 22:25:59 -08:00
Alex Xu
a017cfd00d specify 12.0 instead of 12 2025-12-08 21:31:38 -08:00
Alex Xu
3f4ef3e21e Bump version to 9.3.7 2025-12-08 20:59:26 -08:00
Alex Xu
4733c6348b specify macOS 12 minimum for standard builds (#1188) 2025-12-08 20:58:15 -08:00
Alex Xu
5ad23d9629 mention color 2025-12-08 19:50:34 -08:00
Alex Xu
db4eb78963 Bump version to 9.3.6 2025-12-08 19:15:47 -08:00
Alex Xu
988fc93dc5 Fix macOS 10.14+ legacy compatibility (#1187)
* Update requirements-osx-legacy.txt

* upgrade macos-13 to macos-15-intel

* upgrade macos-13 to macos-15-intel
2025-12-08 18:08:16 -08:00
Alex Xu
74fee9346c Bump version to 9.3.5 2025-12-03 19:15:45 -08:00
Alex Xu
9fcacd7ae6 fix comicinfo detection in corner case (9.3.4 regression) (#1185) 2025-12-03 19:13:19 -08:00
Alex Xu
8ac58e361f Bump version to 9.3.4 2025-12-03 10:23:00 -08:00
kiryl
61d6972e22 Sync setup install_requires with requirements.txt (#1176)
* remove duplicated PyMuPDF entry and change packages order for easier comparison with requirements.txt

* update packages versions to be synced to each other (requirements.txt vs install_requires in setuptools.setup()

* add missing pyinstaller package which is required to build exe/app

* clarify minimums

* fix typo

* remove pyinstaller

Remove pyinstaller from the requirements.

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-12-02 21:13:26 -08:00
Alex Xu
c7c1557e72 add tomenumber when output folder checked (#1183)
* add tomenumber when output checked

* fix all cases
2025-12-02 20:54:46 -08:00
kiryl
cb93704e08 Mention tabulation order in README.md (#1181) 2025-12-02 15:20:52 -08:00
kiryl
62c5183609 set tabulation order for KCC fields (#1178)
* set tabulation order for KCC fields

- loop through fields in more organized order including fields which visibility depends on some checkboxes state

* don't change rc

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-12-02 12:36:17 -08:00
kiryl
a629f267a1 set tabulation order for metadata editor fields (#1177)
* set tabulation order for metadata editor fields

- loop through fields in from top to down order

* don't change rc

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-12-02 12:33:38 -08:00
Alex Xu
aeec4dd294 fix output folder with period (#1180) 2025-12-02 12:04:15 -08:00
Alex Xu
0d3076465b use less file operations (#1174) 2025-12-01 19:27:17 -08:00
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
35 changed files with 2389 additions and 1587 deletions

View File

@@ -1,13 +1,40 @@
.git
.github
build
dist
KindleComicConverter.egg-info
.dockerignore
.gitignore
.travis.yml
Dockerfile
venv
.venv
__pycache__/
*/__pycache__/
*.pyc
*.md
LICENSE.txt
*.txt
!requirements-docker.txt
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,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

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:
workflow_dispatch:
push:
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
tags:
- 'v*.*.*'
# Don't trigger if it's just a documentation update
paths-ignore:
@@ -15,19 +15,53 @@ on:
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build_and_push:
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
with:
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 }}
secrets:
ghcr_token: ${{ secrets.GITHUB_TOKEN }}
build_and_publish_base_image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
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,7 +25,7 @@ jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -35,7 +35,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt
- name: build binary
run: |
@@ -59,7 +59,7 @@ jobs:
env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: AppImage
path: './*.AppImage*'
@@ -68,7 +68,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
*.AppImage*

View File

@@ -25,10 +25,12 @@ jobs:
build:
strategy:
matrix:
os: [ macos-13, macos-14 ]
os: [ macos-15-intel, macos-14 ]
runs-on: ${{ matrix.os }}
env:
MACOSX_DEPLOYMENT_TARGET: '14.0'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -36,7 +38,7 @@ jobs:
cache: 'pip'
- name: Install python dependencies
run: |
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
python -m pip install --upgrade pip pyinstaller certifi
pip install -r requirements.txt
- name: Install the Apple certificate and provisioning profile
# TODO signing
@@ -78,7 +80,7 @@ jobs:
run: |
python setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg
@@ -87,9 +89,8 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile
# TODO signing

View File

@@ -23,7 +23,7 @@ jobs:
build:
strategy:
matrix:
os: [ macos-13 ]
os: [ macos-15-intel ]
runs-on: ${{ matrix.os }}
env:
# We need the official Python, because the GA ones only support newer macOS versions
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- uses: actions/checkout@v5
- 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
@@ -40,7 +40,7 @@ jobs:
- name: Install Python dependencies
run: |
python3 --version
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
pip3 install --upgrade pip pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- uses: actions/setup-node@v6
@@ -51,7 +51,7 @@ jobs:
run: |
python3 setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg
@@ -60,7 +60,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
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@v5
- 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.3
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,9 +23,19 @@ on:
jobs:
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
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -35,20 +45,20 @@ jobs:
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary
run: |
python setup.py build_binary
python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: windows-build
name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.3
uses: signpath/github-action-submit-signing-request@v2.0
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
@@ -63,7 +73,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
dist/*.exe

View File

@@ -27,7 +27,7 @@ jobs:
env:
WINDOWS_7: 1
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -37,7 +37,7 @@ jobs:
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade pip
pip install -r requirements-win7.txt
pip install certifi pyinstaller --no-binary pyinstaller
.\gen_ui_files.bat
@@ -46,15 +46,26 @@ jobs:
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: windows7-build
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.exe

1
.gitignore vendored
View File

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

View File

@@ -1,19 +1,77 @@
# Select final stage based on TARGETARCH ARG
FROM ghcr.io/ciromattia/kcc:docker-base-20241116
LABEL com.kcc.name="Kindle Comic Converter"
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"
# STAGE 1: BUILDER
# Contains all build tools and dev dependencies, will be discarded
FROM python:3.13-slim-bullseye AS builder
COPY . /opt/kcc
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
# Install system dependencies
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" && \
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"]

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 pymupdf
######################################################################################
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

108
README.md
View File

@@ -7,12 +7,19 @@
[![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](https://github.com/ciromattia/kcc/releases)
**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
**Kindle Comic Converter** optimizes black & white (or color) comics and manga for E-ink ereaders
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux.
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.
**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
@@ -26,7 +33,7 @@ which have different requirements than normal LCD screens.
Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small storage capacities.
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:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
@@ -34,6 +41,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
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
The GUI looks like this, built in Qt6, with my most commonly used settings:
@@ -46,7 +54,9 @@ You can change the default output directory by holding `Shift` while clicking th
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
@@ -92,19 +102,19 @@ Click on **Assets** of the latest release.
You probably want either
- `KCC_*.*.*.exe` (Windows)
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 12+)
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
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.
On Mac, right click open to get past the security warning.
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre will break the formatting.
- 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.
@@ -116,6 +126,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
- 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.
- 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?
@@ -175,38 +187,44 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
### Profiles:
```
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Voyage, (1072, 1448), Palette16, 1.8),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
'OTHER': ("Other", (0, 0), Palette16, 1.8),
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
'OTHER': ("Other", (0, 0), Palette16, 1.0),
```
### Standalone `kcc-c2e.py` usage:
@@ -230,6 +248,8 @@ MAIN:
PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
--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
-r SPLITTER, --splitter SPLITTER
@@ -250,9 +270,16 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
--smartcovercrop Attempt to crop main cover from wide image
--coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG
--forcepng Create PNG files instead JPEG for black and white images
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
--force-png-rgb Force color images to be saved as PNG
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
--noquantize Don't quantize PNG images to 16 colors
--mozjpeg Create JPEG files using mozJpeg
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
--maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not recoverable.
@@ -271,7 +298,9 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option.
--rotateright Rotate double page spreads in opposite direction.
--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:
@@ -315,6 +344,7 @@ Depending on your system [Python](https://www.python.org) may be called either `
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
If new objects have been added, verify that correct tab order has been applied by using [Tab Order Editing Mode](https://doc.qt.io/qt-6/designer-tab-order.html).
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

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

@@ -1,16 +0,0 @@
name: kcc
channels:
- conda-forge
- defaults
dependencies:
- python=3.11
- Pillow>=11.3.0
- psutil>=5.9.5
- python-slugify>=1.2.1
- raven>=6.0.0
- distro
- natsort>=8.4.0
- pip
- pip:
- mozjpeg-lossless-optimization>=1.1.2
- pyside6>=6.5.1

1296
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -192,6 +192,18 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>
<tabstop>pencillerLine</tabstop>
<tabstop>inkerLine</tabstop>
<tabstop>coloristLine</tabstop>
<tabstop>okButton</tabstop>
<tabstop>cancelButton</tabstop>
</tabstops>
<resources>
<include location="KCC.qrc"/>
</resources>

View File

@@ -22,7 +22,7 @@ import itertools
from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QTreeView, QAbstractItemView)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os
@@ -42,7 +42,7 @@ from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from .comicarchive import SEVENZIP, TAR, available_archive_tools
from . import __version__
from . import comic2ebook
from . import metadata
@@ -195,7 +195,7 @@ class VersionThread(QThread):
icon = 'bindle'
if category == 'kofi':
icon = 'kofi'
message = f"<b>{payload.get('name')}</b>"
message = f"{payload.get('name')}"
if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'):
@@ -327,6 +327,14 @@ class WorkerThread(QThread):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.pdfExtractBox.isChecked():
options.pdfextract = 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:
@@ -341,12 +349,24 @@ class WorkerThread(QThread):
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())
@@ -382,13 +402,14 @@ class WorkerThread(QThread):
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 job in currentJobs:
for i, job in enumerate(currentJobs, start=1):
job_progress_number = f'[{i}/{len(currentJobs)}] '
sleep(0.5)
if not self.conversionAlive:
self.clean()
return
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':
MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files'
@@ -402,7 +423,7 @@ class WorkerThread(QThread):
jobargv.append(job)
try:
comic2ebook.options = comic2ebook.checkOptions(copy(options))
outputPath = comic2ebook.makeBook(job, self)
outputPath = comic2ebook.makeBook(job, self, job_progress_number)
MW.hideProgressBar.emit()
except UserWarning as warn:
if not self.conversionAlive:
@@ -444,7 +465,7 @@ class WorkerThread(QThread):
else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
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('tick')
MW.addMessage.emit('Creating MOBI files', 'info', False)
@@ -518,11 +539,12 @@ class WorkerThread(QThread):
if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi'))
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
MW.addMessage.emit(self.kindlegenErrorCode[1], 'error', False)
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
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',
False)
if self.kindlegenErrorCode[0] == 3221226505:
@@ -609,12 +631,30 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Comic (*.pdf);;All (*.*)')
for fname in fnames[0]:
if fname != '':
if sys.platform.startswith('win'):
fname = fname.replace('/', '\\')
self.lastPath = os.path.abspath(os.path.join(fname, os.pardir))
GUI.jobList.addItem(fname)
GUI.jobList.scrollToBottom()
def selectDir(self):
if self.needClean:
self.needClean = False
GUI.jobList.clear()
dialog = QFileDialog(MW, 'Select input folder(s)', self.lastPath)
dialog.setFileMode(QFileDialog.FileMode.Directory)
dialog.setOption(QFileDialog.Option.ShowDirsOnly, True)
dialog.setOption(QFileDialog.Option.DontUseNativeDialog, True)
dialog.findChild(QTreeView).setSelectionMode(QAbstractItemView.ExtendedSelection)
if dialog.exec():
dnames = dialog.selectedFiles()
for dname in dnames:
if dname != '':
self.lastPath = os.path.abspath(os.path.join(dname, os.pardir))
GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom()
def selectFileMetaEditor(self, sname):
if not sname:
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
@@ -656,6 +696,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
def openHumble(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://humblebundleinc.sjv.io/3JaR3A'))
def openYouTube(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://www.youtube.com/@eink-dude'))
def openDiscord(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://discord.gg/um5JRKwmGT'))
def modeChange(self, mode):
if mode == 1:
self.currentMode = 1
@@ -723,6 +775,12 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingWidget.setVisible(False)
self.changeCroppingPower(100) # 1.0
def togglejpegqualityBox(self, value):
if value:
GUI.jpegQualityWidget.setVisible(True)
else:
GUI.jpegQualityWidget.setVisible(False)
def togglewebtoonBox(self, value):
if value:
self.addMessage('You can choose a taller device profile to get taller cuts in webtoon mode.', 'info')
@@ -753,7 +811,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.rotateBox.setEnabled(True)
GUI.borderBox.setEnabled(True)
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if profile['Label'] != 'KS':
if not profile['Label'].startswith('KS'):
GUI.upscaleBox.setEnabled(True)
GUI.croppingBox.setEnabled(True)
GUI.interPanelCropBox.setEnabled(True)
@@ -778,12 +836,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def toggleImageFormatBox(self, value):
profile = GUI.profiles[str(GUI.deviceBox.currentText())]
if value == 1:
if profile['Label'] == 'KS':
if profile['Label'].startswith('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
GUI.pngLegacyBox.setEnabled(True)
GUI.noQuantizeBox.setEnabled(True)
GUI.forcePngRgbBox.setEnabled(True)
else:
GUI.pngLegacyBox.setEnabled(False)
GUI.noQuantizeBox.setEnabled(False)
GUI.forcePngRgbBox.setEnabled(False)
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
@@ -840,7 +906,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if not GUI.webtoonBox.isChecked():
GUI.qualityBox.setEnabled(profile['PVOptions'])
GUI.upscaleBox.setChecked(profile['DefaultUpscale'])
if profile['Label'] == 'KS':
if profile['Label'].startswith('KS'):
GUI.upscaleBox.setDisabled(True)
else:
if not GUI.webtoonBox.isChecked():
@@ -851,6 +917,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
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
elif profile['Label'] == 'KDX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
if not profile['PVOptions']:
GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other':
@@ -876,6 +946,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.chunkSizeCheckBox.setChecked(False)
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():
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
def stripTags(self, html):
s = HTMLStripper()
@@ -1003,8 +1075,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(),
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
'jpegQuality': GUI.jpegQualitySpinBox.value(),
'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState(),
@@ -1012,6 +1094,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(),
'rotateRightBox': GUI.rotateRightBox.checkState(),
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100,
@@ -1025,7 +1108,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
MW.activateWindow()
if type(message) is bytes:
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:
self.needClean = False
GUI.jobList.clear()
@@ -1056,6 +1139,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if message[-1] == '/':
message = message[:-1]
self.handleMessage(message)
# sorting may conflict with manual file fusion order
# GUI.jobList.sortItems()
def forceShutdown(self):
self.saveSettings(None)
@@ -1132,7 +1217,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'gridLayout_6', 'horizontalLayout_2']:
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
if self.windowSize == '0x0':
MW.resize(500, 500)
@@ -1142,6 +1227,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1158,9 +1244,24 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW34'},
"Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0,
'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KV'},
"Kindle Scribe": {
"Kindle 1860x1920": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1860',
},
"Kindle 1920x1920": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1920',
},
"Kindle 1240x1860": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
},
"Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
"Kindle Scribe 3": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
},
"Kindle Scribe Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
},
"Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
},
@@ -1235,9 +1336,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'OTHER'},
}
profilesGUI = [
"Kindle Scribe Colorsoft",
"Kindle Scribe 3",
"Kindle Colorsoft",
"Kindle Paperwhite 12",
"Kindle Scribe",
"Kindle Scribe 1/2",
"Kindle Paperwhite 11",
"Kindle 11",
"Kindle Oasis 9/10",
@@ -1257,6 +1360,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Other",
"Separator",
"Kindle 1920x1920",
"Kindle 1860x1920",
"Kindle 1240x1860",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle Paperwhite 7/10",
@@ -1304,7 +1410,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
'info')
self.tar = 'tar' in available_archive_tools()
self.tar = TAR in available_archive_tools()
self.sevenzip = SEVENZIP in available_archive_tools()
if not any([self.tar, self.sevenzip]):
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
@@ -1315,14 +1421,19 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.defaultOutputFolderButton.clicked.connect(self.selectDefaultOutputFolder)
GUI.clearButton.clicked.connect(self.clearJobs)
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.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
GUI.croppingPowerSlider.valueChanged.connect(self.changeCroppingPower)
GUI.jpegQualityBox.stateChanged.connect(self.togglejpegqualityBox)
GUI.webtoonBox.stateChanged.connect(self.togglewebtoonBox)
GUI.qualityBox.stateChanged.connect(self.togglequalityBox)
GUI.mozJpegBox.stateChanged.connect(self.toggleImageFormatBox)
@@ -1384,6 +1495,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingPowerSlider.setValue(int(self.options[option]))
self.changeCroppingPower(int(self.options[option]))
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
elif str(option) == "jpegQuality":
GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
elif str(option) == "chunkSizeBox":
GUI.chunkSizeBox.setValue(int(self.options[option]))
else:

File diff suppressed because it is too large Load Diff

View File

@@ -156,6 +156,15 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
QWidget.setTabOrder(self.pencillerLine, self.inkerLine)
QWidget.setTabOrder(self.inkerLine, self.coloristLine)
QWidget.setTabOrder(self.coloristLine, self.okButton)
QWidget.setTabOrder(self.okButton, self.cancelButton)
self.retranslateUi(editorDialog)

View File

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

View File

@@ -43,11 +43,12 @@ from psutil import virtual_memory, disk_usage
from html import escape as hescape
import pymupdf
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -65,18 +66,29 @@ def main(argv=None):
parser.print_help()
return 0
if sys.platform.startswith('win'):
sources = set([source for option in options.input for source in glob(escape(option))])
sources = [source for option in options.input for source in glob(escape(option))]
else:
sources = set(options.input)
sources = options.input
if len(sources) == 0:
print('No matching files found.')
return 1
if options.filefusion:
fusion_path = makeFusion(list(sources))
sources.clear()
sources.append(fusion_path)
for source in sources:
source = source.rstrip('\\').rstrip('/')
options = copy(args)
options = checkOptions(options)
print('Working on ' + 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
@@ -341,6 +353,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
elif '.webp' == filename[1]:
mt = 'image/webp'
else:
mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
@@ -445,7 +459,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
"</container>"])
f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, len_tomes=0):
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, job_progress='', len_tomes=0):
filelist = []
chapterlist = []
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
@@ -553,7 +567,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
else:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
build_html_end = perf_counter()
print(f"buildHTML: {build_html_end - build_html_start} seconds")
print(f"{job_progress}buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if ComicInfo.xml has bookmarks
if ischunked:
options.comicinfo_chapters = []
@@ -589,7 +603,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
buildOPF(path, options.title, filelist, originalpath, cover)
def buildPDF(path, title, cover=None, output_file=None):
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.
@@ -614,11 +628,11 @@ def buildPDF(path, title, cover=None, output_file=None):
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
print(f"MuPDF output: {end-start} sec")
print(f"{job_progress}MuPDF output: {end-start} sec")
return output_file
def imgDirectoryProcessing(path):
def imgDirectoryProcessing(path, job_progress=''):
global workerPool, workerOutput
workerPool = Pool(maxtasksperchild=100)
workerOutput = []
@@ -638,7 +652,7 @@ def imgDirectoryProcessing(path):
workerPool.close()
workerPool.join()
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
dot_clean(path)
@@ -648,7 +662,7 @@ def imgDirectoryProcessing(path):
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("C2E: Source directory is empty.")
@@ -677,7 +691,6 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
@@ -687,18 +700,24 @@ def imgFileProcessing(work):
img.gammaCorrectImage()
if not img.colorOutput:
img.convertToGrayscale()
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.eraserainbow, is_color)
img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
if is_color:
if img.colorOutput:
pass
elif opt.forcepng:
img.convertToGrayscale()
if opt.format != 'PDF':
if not opt.noquantize:
img.quantizeImage()
else:
img.convertToGrayscale()
if opt.format == 'PDF':
img.convertToGrayscale()
elif opt.profile == 'KDX' and opt.format == 'CBZ':
img.convertToGrayscale()
elif opt.pnglegacy:
img.convertToGrayscale()
output.append(img.saveToDir())
return output
except Exception:
@@ -728,7 +747,9 @@ def render_page(vector):
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
target_height = vector[4]
target_width = vector[4]
target_height = vector[5]
pdf_width = vector[6]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
@@ -739,7 +760,10 @@ def render_page(vector):
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
zoom = target_height / page.rect.height
if not pdf_width or page.rect.width > page.rect.height:
zoom = target_height / page.rect.height
else:
zoom = target_width / page.rect.width
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
@@ -789,9 +813,7 @@ def extract_page(vector):
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)
continue
else:
xref = image_list[0][0]
d = doc.extract_image(xref)
@@ -806,7 +828,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
@@ -826,7 +848,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
cpu = cpu_count()
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
@@ -840,14 +862,17 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def getWorkFolder(afile):
def getWorkFolder(afile, workdir=None):
if not workdir:
workdir = mkdtemp('', 'KCC-')
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else:
fullPath = workdir
if os.path.isdir(afile):
if disk_usage(gettempdir())[2] < getDirectorySize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
try:
os.rmdir(workdir)
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
copytree(afile, fullPath)
sanitizePermissions(fullPath)
return workdir
@@ -858,48 +883,60 @@ def getWorkFolder(afile):
if disk_usage(gettempdir())[2] < os.path.getsize(afile) * 2.5:
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
if not os.path.exists(fullPath):
os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
target_height = options.profileData[1][1]
if options.pdfextract:
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
njpg = pdf.extract()
if njpg == 0:
raise UserWarning("Failed to extract images from PDF file.")
return workdir
target_width, target_height = options.profileData[1]
if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
target_height *= 1.2 #Account for possible margin at the top and bottom
target_width *= 1.2
elif options.cropping == 2:
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
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, workdir, target_height)
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
return workdir
else:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
if not os.path.exists(fullPath):
os.makedirs(fullPath)
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
path = cbx.extract(fullPath)
sanitizePermissions(path)
tdir = os.listdir(workdir)
tdir = os.listdir(fullPath)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
if os.path.isdir(os.path.join(fullPath, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
os.path.join(fullPath, 'ComicInfo.xml'),
os.path.join(fullPath, 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])
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
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]))
return workdir
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
else:
raise UserWarning("Failed to open source file/directory.")
newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
return newpath
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
source_path = Path(srcpath)
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if 'Ko' in options.profile and options.format == 'EPUB':
@@ -909,20 +946,29 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
else:
ext = '.kepub.epub'
if wantedname is not None:
wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif os.path.isdir(srcpath):
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
elif wanted_ext == '.mobi' and ext == '.epub':
filename = os.path.abspath(wanted_root + ext)
# output directory
else:
filename = os.path.join(os.path.abspath(options.output),
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
abs_path = os.path.abspath(options.output)
if not os.path.exists(abs_path):
os.mkdir(abs_path)
if source_path.is_file():
filename = os.path.join(os.path.abspath(options.output), source_path.stem + tomenumber + ext)
else:
filename = os.path.join(os.path.abspath(options.output), source_path.name + tomenumber + ext)
elif os.path.isdir(srcpath):
filename = srcpath + tomenumber + ext
else:
if 'Ko' in options.profile and options.format == 'EPUB':
src = pathlib.Path(srcpath)
name = re.sub(r'\W+', '_', src.stem) + tomenumber + ext
filename = src.with_name(name)
if source_path.is_file():
name = re.sub(r'\W+', '_', source_path.stem) + tomenumber + ext
else:
name = re.sub(r'\W+', '_', source_path.name) + tomenumber + ext
filename = source_path.with_name(name)
else:
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
if os.path.isfile(filename):
@@ -931,6 +977,13 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
elif options.format == 'MOBI' and ext == '.epub':
counter = 0
basename = os.path.splitext(filename)[0]
if os.path.isfile(basename + '.mobi'):
while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
return filename
@@ -1028,7 +1081,7 @@ def removeNonImages(filetree):
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
if ext not in IMAGE_TYPES:
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
# remove empty nested folders
@@ -1040,7 +1093,7 @@ def removeNonImages(filetree):
raise UserWarning('No images detected, nested archives are not supported.')
def sanitizeTree(filetree):
def sanitizeTree(filetree, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1050,7 +1103,7 @@ def sanitizeTree(filetree):
_, ext = getImageFileName(name)
# 9999 page limit
unique_name = f'kcc-{page:04}'
unique_name = f'{prefix}-{page:04}'
page += 1
newKey = os.path.join(root, unique_name + ext)
@@ -1099,7 +1152,7 @@ def chunk_directory(path):
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files:
# Windows MAX_LEN = 260 plus some buffer
if os.name == 'nt' and len(os.path.join(root, f)) > 180:
if os.name == 'nt' and len(os.path.join(root, f)) > 220:
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1
break
@@ -1211,7 +1264,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
GUI.addMessage.emit('Source files are probably created by KCC. The second conversion will decrease quality.'
, 'warning', False)
GUI.addMessage.emit('', '', False)
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and options.profile != 'KS':
if imageSmaller > imageNumber * 0.25 and not options.upscale and not options.stretch and not options.profile.startswith('KS'):
print("WARNING: More than 25% of images are smaller than target device resolution. "
"Consider enabling stretching or upscaling to improve readability.")
if GUI:
@@ -1238,17 +1291,20 @@ 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))
return value
def makeZIP(zipfilename, basedir, isepub=False):
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
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')
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
mimetypeFile.close()
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
subprocess_run([SEVENZIP, 'a', '-mx0', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
# crazy hack to ensure mimetype is first when using 7zip
if isepub:
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
@@ -1259,7 +1315,7 @@ def makeZIP(zipfilename, basedir, isepub=False):
zipOutput.write(path, aPath)
zipOutput.close()
end = perf_counter()
print(f"makeZIP time: {end - start} seconds")
print(f"{job_progress}makeZIP time: {end - start} seconds")
return zipfilename
def makeParser():
@@ -1311,11 +1367,21 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
help="Rotate double page spreads in opposite direction.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
help="Render vector PDFs to device width instead of height.")
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
help="Attempt to crop main cover from wide image")
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
@@ -1330,6 +1396,8 @@ def makeParser():
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",
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",
@@ -1349,9 +1417,19 @@ def makeParser():
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
help="Create PNG files instead JPEG for black and white images")
processing_options.add_argument("--force-png-rgb", action="store_true", dest="force_png_rgb", default=False,
help="Force color images to be saved as PNG")
processing_options.add_argument("--webp", action="store_true", dest="webp", default=False,
help="Replace JPG with lossy WEBP and PNG with lossless WEBP")
processing_options.add_argument("--pnglegacy", action="store_true", dest="pnglegacy", default=False,
help="Use a more compatible 8 bit png instead of 4 bit")
processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
help="Don't quantize to 16 color PNG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
help="Create JPEG files using mozJpeg")
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
help="The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.")
processing_options.add_argument("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
help="Turn 1x4 strips to 2x2 strips")
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
@@ -1374,6 +1452,11 @@ def checkOptions(options):
options.isKobo = False
options.bordersColor = None
options.keep_epub = False
if options.format == 'PDF-200MB':
options.targetsize = 195
options.format = 'PDF'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'EPUB-200MB':
options.targetsize = 195
options.format = 'EPUB'
@@ -1385,6 +1468,8 @@ def checkOptions(options):
options.format = 'MOBI'
if options.batchsplit != 2:
options.batchsplit = 1
if not options.targetsize and options.profile.startswith('Rmk'):
options.targetsize = 95
if options.format == 'MOBI+EPUB':
options.keep_epub = True
options.format = 'MOBI'
@@ -1430,9 +1515,6 @@ def checkOptions(options):
if 'Ko' in options.profile:
options.panelview = False
options.hq = False
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.customheight = 1200
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
if options.format == 'KFX':
options.format = 'EPUB'
@@ -1451,6 +1533,26 @@ def checkOptions(options):
image.ProfileData.Profiles["Custom"] = newProfile
options.profile = "Custom"
options.profileData = image.ProfileData.Profiles[options.profile]
if not options.jpegquality:
if options.profile.startswith('KS') or options.profile == 'KCS':
options.jpegquality = 90
else:
options.jpegquality = 85
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
options.webp_output = options.format != 'PDF' and not options.kindle_azw3 and options.webp
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1])
options.profileData[1][1] = 1200
if options.kindle_scribe_azw3:
options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1])
options.profileData[1][0] = min(1920, options.profileData[1][0])
return options
@@ -1498,21 +1600,28 @@ def makeFusion(sources: List[str]):
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
print("Running Fusion")
for source in sources:
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
needs_prefix = os_sorted(path_names) != path_names
for index, source in enumerate(sources, start=1):
print(f"Processing {source}...")
checkPre(source)
print("Checking images...")
path = getWorkFolder(source)
pathfinder = os.path.join(path, "OEBPS", "Images")
sanitizeTree(pathfinder)
# TODO: remove flattenTree when subchapters are supported
flattenTree(pathfinder)
source_path = Path(source)
# Add the fusion_0001_ prefix to maintain user-specified order if needed
prefix = ''
if needs_prefix:
prefix = f'fusion_{index:04d}_'
if source_path.is_file():
os.renames(pathfinder, fusion_path.joinpath(source_path.stem))
targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
else:
os.renames(pathfinder, fusion_path.joinpath(source_path.name))
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
getWorkFolder(source, str(targetpath))
sanitizeTree(targetpath, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath)
end = perf_counter()
print(f"makefusion: {end - start} seconds")
@@ -1521,7 +1630,7 @@ def makeFusion(sources: List[str]):
return str(fusion_path)
def makeBook(source, qtgui=None):
def makeBook(source, qtgui=None, job_progress=''):
start = perf_counter()
global GUI
GUI = qtgui
@@ -1529,29 +1638,31 @@ def makeBook(source, qtgui=None):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
checkPre(source)
print("Preparing source images...")
print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source)
print("Checking images...")
print(f"{job_progress}Checking images...")
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'))
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()}
cover = None
if not options.webtoon:
cover = image.Cover(cover_path, options)
if options.webtoon:
x, y = image.ProfileData.Profiles[options.profile][1]
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], qtgui)
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
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:
print("Processing images...")
print(f"{job_progress}Processing images...")
if GUI:
GUI.progressBarTick.emit('Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
GUI.progressBarTick.emit(f'{job_progress}Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"), job_progress)
if GUI:
GUI.progressBarTick.emit('1')
if options.batchsplit > 0 or options.targetsize:
@@ -1562,11 +1673,11 @@ def makeBook(source, qtgui=None):
tomeNumber = 0
if GUI:
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('Creating PDF files')
GUI.progressBarTick.emit(f'{job_progress}Creating PDF files')
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('tick')
options.baseTitle = options.title
@@ -1580,29 +1691,29 @@ def makeBook(source, qtgui=None):
tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.format == 'CBZ':
print("Creating CBZ file...")
print(f"{job_progress}Creating CBZ file...")
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
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("Creating PDF file with PyMuPDF...")
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, None, output_file)
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
filepath.append(output_pdf)
else:
print("Creating EPUB file...")
print(f"{job_progress}Creating EPUB file...")
if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, len(tomes))
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, job_progress, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source)
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True)
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])
@@ -1615,23 +1726,23 @@ def makeBook(source, qtgui=None):
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI':
print("Creating MOBI files...")
print(f"{job_progress}Creating MOBI files...")
work = []
for i in filepath:
work.append([i])
output = makeMOBI(work, GUI)
for errors in output:
if errors[0] != 0:
print('Error: KindleGen failed to create MOBI!')
print(f"{job_progress}Error: KindleGen failed to create MOBI!")
print(errors)
return filepath
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
print("Kindle detected. Uploading covers...")
print(f"{job_progress}Kindle detected. Uploading covers...")
for i in filepath:
output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
if not output[0]:
print('Error: Failed to tweak KindleGen output!')
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
return filepath
else:
os.remove(i.replace('.epub', '.mobi') + '_toclean')
@@ -1644,7 +1755,7 @@ def makeBook(source, qtgui=None):
rmtree(source, True)
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)
@@ -1729,4 +1840,3 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput

View File

@@ -24,10 +24,12 @@ import sys
from argparse import ArgumentParser
from shutil import rmtree
from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
from PIL.Image import Dither
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
ImageFile.LOAD_TRUNCATED_IMAGES = True
def mergeDirectoryTick(output):
if output:
@@ -60,18 +62,19 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072 * 2:
raise RuntimeError(f'Image too tall at {targetHeight} pixels.')
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
for i in imagesValid:
img = Image.open(i).convert('RGB')
if img.size[0] < targetWidth or img.size[0] > targetWidth:
widthPercent = (targetWidth / float(img.size[0]))
heightSize = int((float(img.size[1]) * float(widthPercent)))
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
result.paste(img, (0, y))
y += img.size[1]
with Image.open(i) as img:
img = img.convert('RGB')
if img.size[0] < targetWidth or img.size[0] > targetWidth:
widthPercent = (targetWidth / float(img.size[0]))
heightSize = int((float(img.size[1]) * float(widthPercent)))
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
result.paste(img, (0, y))
y += img.size[1]
os.remove(i)
savePath = os.path.split(imagesValid[0])
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
@@ -219,7 +222,7 @@ def splitImage(work):
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
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
@@ -251,16 +254,14 @@ def main(argv=None, qtgui=None):
return 1
if args.height > 0:
for sourceDir in args.input:
targetDir = sourceDir + "-Splitted"
targetDir = sourceDir
if os.path.isdir(sourceDir):
rmtree(targetDir, True)
os.renames(sourceDir, targetDir)
work = []
pagenumber = 1
splitWorkerOutput = []
splitWorkerPool = Pool(maxtasksperchild=10)
if args.merge:
print("Merging images...")
print(f"{job_progress}Merging images...")
directoryNumer = 1
mergeWork = []
mergeWorkerOutput = []
@@ -272,7 +273,7 @@ def main(argv=None, qtgui=None):
directoryNumer += 1
mergeWork.append([os.path.join(root, directory)])
if GUI:
GUI.progressBarTick.emit('Combining images')
GUI.progressBarTick.emit(f'{job_progress}Combining images')
GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
@@ -285,7 +286,7 @@ def main(argv=None, qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1])
print("Splitting images...")
print(f"{job_progress}Splitting images...")
dot_clean(targetDir)
for root, _, files in os.walk(targetDir, False):
for name in files:
@@ -295,7 +296,7 @@ def main(argv=None, qtgui=None):
else:
os.remove(os.path.join(root, name))
if GUI:
GUI.progressBarTick.emit('Splitting images')
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick')
if len(work) > 0:
@@ -311,8 +312,6 @@ def main(argv=None, qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
splitWorkerOutput[0][1])
if args.inPlace:
os.renames(targetDir, sourceDir)
else:
rmtree(targetDir, True)
raise UserWarning("C2P: Source directory is empty.")

View File

@@ -20,15 +20,17 @@
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
from .shared import subprocess_run
from .shared import IMAGE_TYPES, subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
class ComicArchive:
@@ -65,11 +67,14 @@ class ComicArchive:
def extract(self, targetdir):
if not os.path.isdir(targetdir):
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 = []
extraction_commands = [
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-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.basename],
]
@@ -121,7 +126,7 @@ class ComicArchive:
def available_archive_tools():
available = []
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
available.append(tool)

View File

@@ -24,13 +24,14 @@ import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from 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 .inter_panel_crop_alg import crop_empty_inter_panel
AUTO_CROP_THRESHOLD = 0.015
ImageFile.LOAD_TRUNCATED_IMAGES = True
class ProfileData:
@@ -85,6 +86,9 @@ class ProfileData:
]
ProfilesKindleEBOK = {
}
ProfilesKindlePDOC = {
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
@@ -92,16 +96,18 @@ class ProfileData:
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
}
ProfilesKindlePDOC = {
'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),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
}
ProfilesKindle = {
@@ -152,7 +158,7 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
# Image.open(srcImgPath).verify()
with Image.open(srcImgPath) as im:
self.image = im.copy()
@@ -191,7 +197,10 @@ class ComicPageParser:
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
@@ -212,7 +221,10 @@ class ComicPageParser:
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.fill])
@@ -262,10 +274,11 @@ class ComicPage:
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.original_color_mode = image.mode
# TODO: color check earlier
self.image = image.convert("RGB")
self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
@@ -284,8 +297,7 @@ class ComicPage:
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@cached_property
def color(self):
def colorCheck(self):
if self.original_color_mode in ("L", "1"):
return False
if self.opt.webtoon:
@@ -296,9 +308,9 @@ class ComicPage:
# 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, cr, cutoff=(2, 2)):
cb_hist = cb.histogram()
cr_hist = cr.histogram()
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
@@ -327,60 +339,50 @@ class ComicPage:
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_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment, don't go lower than 7
SPREAD_THRESHOLD = 7
if self.opt.forcecolor:
if any([
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()
# get rid of some jpg compression
cutoff = (.2, .2)
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment
SPREAD_THRESHOLD = 5
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
# check for large amount of extreme colors
# 11 if too high. 10 is barely enough. If needed make it magnitude of both
DIFF_THRESHOLD = 10
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
# get ride of most jpg compression
cutoff = (2, 2)
cb_hist, cr_hist = self.histograms_cutoff(cb, cr, cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment
SPREAD_THRESHOLD = 5
if not self.opt.forcecolor and cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
# check for any amount of mild colors still remaining
DIFF_THRESHOLD = 6
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
else:
return False
# 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):
try:
@@ -404,25 +406,32 @@ class ComicPage:
raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath):
if self.opt.forcepng:
image.info["transparency"] = None
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
image.info.pop('transparency', None)
if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
elif self.opt.kindle_azw3:
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else:
targetPath += '.jpg'
if self.opt.mozjpeg:
if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
elif self.opt.mozjpeg:
targetPath += '.jpg'
with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=85)
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
image.save(targetPath, 'JPEG', optimize=1, quality=85)
targetPath += '.jpg'
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath
def gammaCorrectImage(self):
@@ -490,6 +499,14 @@ 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:
# 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_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()
@@ -498,7 +515,9 @@ class ComicPage:
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
@@ -506,7 +525,7 @@ class ComicPage:
self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self):
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
return Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
@@ -557,15 +576,21 @@ class Cover:
def process(self):
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:
self.image = self.image.convert('L')
self.crop_main_cover()
if self.options.smartcovercrop:
self.crop_main_cover()
size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3:
size[0] = min(size[0], 1920)
size[1] = min(size[1], 1920)
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
if self.options.coverfill and not self.options.kindle_scribe_azw3:
# TODO: Kindle Scribe case
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
else:
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
def crop_main_cover(self):
w, h = self.image.size
@@ -583,7 +608,7 @@ class Cover:
def save_to_epub(self, target, tomeid, len_tomes=0):
try:
if tomeid == 0:
self.image.save(target, "JPEG", optimize=1, quality=85)
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
else:
copy = self.image.copy()
draw = ImageDraw.Draw(copy)
@@ -597,7 +622,7 @@ class Cover:
stroke_fill=0,
stroke_width=25
)
copy.save(target, "JPEG", optimize=1, quality=85)
copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
except IOError:
raise RuntimeError('Failed to save cover.')
@@ -605,6 +630,6 @@ class Cover:
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
try:
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=self.options.jpegquality)
except IOError:
raise RuntimeError('Failed to upload cover.')

View File

@@ -1,8 +1,10 @@
from PIL import Image, ImageFilter, ImageOps
from PIL import Image, ImageFilter, ImageOps, ImageFile
import numpy as np
from typing import Literal
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).

View File

@@ -1,7 +1,9 @@
from PIL import ImageOps, ImageFilter
from PIL import ImageOps, ImageFilter, ImageFile
import numpy as np
from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
Some assupmptions on the page number sizes
@@ -158,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
if not edge.height or not edge.width:
continue
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)

View File

@@ -0,0 +1,75 @@
# -*- 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
# 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, fullPath):
self.fname = fname
self.path = fullPath
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
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(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
jpgfile.write(jpg)
jpgfile.close()
njpg += 1
return njpg

View File

@@ -1,5 +1,8 @@
import numpy as np
from PIL import Image
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
def fourier_transform_image(img):
"""

View File

@@ -27,6 +27,9 @@ import sys
from traceback import format_tb
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
class HTMLStripper(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)

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

@@ -1,4 +1,4 @@
PySide6==6.5.2
PySide6==6.4.3
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0

View File

@@ -2,7 +2,7 @@ PySide6<6.10
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
python-slugify>=1.2.1,<9.0.0
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0

View File

@@ -8,6 +8,8 @@ Install as Python package:
Create EXE/APP:
python3 setup.py build_binary
python3 setup.py build_c2e
python3 setup.py build_c2p
"""
import os
@@ -38,8 +40,8 @@ class BuildBinaryCommand(setuptools.Command):
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')
# 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:
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
if min_os.startswith('10.1'):
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')
@@ -57,10 +59,75 @@ class BuildBinaryCommand(setuptools.Command):
else:
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(
cmdclass={
'build_binary': BuildBinaryCommand,
'build_c2e': BuildC2ECommand,
'build_c2p': BuildC2PCommand,
},
name=NAME,
version=VERSION,
@@ -81,16 +148,15 @@ setuptools.setup(
},
packages=['kindlecomicconverter'],
install_requires=[
'pyside6>=6.0.0',
'PySide6>=6.0.0',
'Pillow>=9.3.0',
'PyMuPDF>=1.18.0',
'psutil>=5.9.5',
'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'requests>=2.31.0',
'mozjpeg-lossless-optimization>=1.1.2',
'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0',
'distro',
'distro>=1.8.0',
'numpy>=1.22.4',
'PyMuPDF>=1.16.1',
],