1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-24 18:09:01 +00:00

Compare commits

..

46 Commits

Author SHA1 Message Date
Alex Xu
8a862f11ac bump to 9.1.0 2025-09-11 14:20:09 -07:00
Alex Xu
c32620cfeb Add manga bundle announcements (#1083)
* add announcements

* fix url

* don't check version if frozen

* comment

* update

* use bindle and python 3.11

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

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

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

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

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

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

* windows-2022

* downgrade

* bat

* lower requirements

* downgrade pyside6

* downgrade pyside6 more

* delete

* fix win7

* don't crash when settings load fails

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

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

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

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

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

* optimize

* use with statement

* OS_SORT_KEY

* fix import

* simplify

* fix None

* fix conditional

---------

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

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

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

* make edge_bbox neater
2025-07-26 07:26:10 -07:00
Alex Xu
27296565a3 display non kindle only 2025-07-26 07:17:45 -07:00
Alex Xu
bb70337a35 crop_main_cover ratio adjustment 2025-07-26 07:12:44 -07:00
Alex Xu
cf0586ae70 Update README.md 2025-07-22 17:27:09 -07:00
Alex Xu
4100141b46 Update README.md 2025-07-22 10:38:43 -07:00
Alex Xu
8eb81b7d67 fix 5.15.3 typo 2025-07-22 10:38:02 -07:00
Alex Xu
e2dbc05a83 add OCLP note 2025-07-22 10:36:47 -07:00
Alex Xu
50b82786a1 install pymupdf on armv7 2025-07-22 09:34:59 -07:00
Alex Xu
88d1643f64 Update README.md 2025-07-20 22:56:55 -07:00
Alex Xu
ddf2fa360f Update README.md 2025-07-20 22:56:15 -07:00
Alex Xu
43e974f20d add humble/fanatical pdf note 2025-07-20 22:55:15 -07:00
28 changed files with 2677 additions and 767 deletions

View File

@@ -38,7 +38,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View File

@@ -25,9 +25,9 @@ jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'

View File

@@ -28,9 +28,9 @@ jobs:
os: [ macos-13, macos-14 ] os: [ macos-13, macos-14 ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'
@@ -69,7 +69,7 @@ jobs:
# apply provisioning profile # apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- uses: actions/setup-node@v4 - uses: actions/setup-node@v5
with: with:
node-version: 16 node-version: 16
- run: npm install -g appdmg - run: npm install -g appdmg

View File

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

View File

@@ -20,7 +20,7 @@ jobs:
capital: KCC_c2p capital: KCC_c2p
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Package Application - name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main uses: JackMcKew/pyinstaller-action-windows@main
@@ -40,7 +40,7 @@ jobs:
path: dist/windows/*.exe path: dist/windows/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2 uses: signpath/github-action-submit-signing-request@v1.3
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'

View File

@@ -25,9 +25,9 @@ jobs:
build: build:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v6
with: with:
python-version: 3.11 python-version: 3.11
cache: 'pip' cache: 'pip'
@@ -48,7 +48,7 @@ jobs:
name: windows-build name: windows-build
path: dist/*.exe path: dist/*.exe
- id: optional_step_id - id: optional_step_id
uses: signpath/github-action-submit-signing-request@v1.2 uses: signpath/github-action-submit-signing-request@v1.3
if: ${{ github.repository == 'ciromattia/kcc' }} if: ${{ github.repository == 'ciromattia/kcc' }}
with: with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'

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

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

2
.gitignore vendored
View File

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

View File

@@ -126,7 +126,7 @@ RUN set -x && \
# Install required python modules # Install required python modules
python -m pip install --upgrade pip && \ python -m pip install --upgrade pip && \
python -m venv /opt/venv && \ python -m venv /opt/venv && \
python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy python -m pip install --upgrade pillow psutil requests python-slugify raven packaging mozjpeg-lossless-optimization natsort distro numpy pymupdf
###################################################################################### ######################################################################################

View File

@@ -12,10 +12,14 @@ like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, Pages display in fullscreen without margins,
with proper fixed layout support. with proper fixed layout support.
Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs. Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ. Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
If your source are super high resolution DRM-free PDFs from Kodansha/Humble Bundle/Fanatical, **NEW**: PDF output is now supported for direct conversion to reMarkable devices!
you'll need to first [convert the PDFs to CBZ](https://github.com/ciromattia/kcc/issues/680) for use in KCC. When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
which can be directly converted by KCC.
Its main feature is various optional image processing steps to look good on eink screens, Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens. which have different requirements than normal LCD screens.
@@ -94,7 +98,7 @@ The `c2e` and `c2p` versions are command line tools for power users.
On Windows 11, you may need to run in compatibility mode for an older Windows version. On Windows 11, you may need to run in compatibility mode for an older Windows version.
On Mac, right click open to get past the security warning. On Mac, right click open to get past the security warning. macOS 12 Monterey or later is required, you can use https://dortania.github.io/OpenCore-Legacy-Patcher/ to get a newer macOS on unsupported hardware.
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
@@ -102,9 +106,11 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
- Should I use Calibre? - Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output in Calibre will break the formatting. - No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output in Calibre will break the formatting.
Viewing KCC output in Calibre will also not work properly. Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.16.3+, you can get cover thumbnails simply by USB dropping into documents folder. On 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. On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended. If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo.
- All options have additional information in tooltips if you hover over the option. - All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB - To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Right to left mode not working? - Right to left mode not working?
@@ -117,11 +123,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
- How to make AZW3 instead of MOBI? - How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons. - The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678) - [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- [Combine files/chapters](https://github.com/ciromattia/kcc/issues/612#issuecomment-2117985011)
- [Flatpak mobi conversion stuck](https://github.com/ciromattia/kcc/wiki/Installation#linux)
- Image too dark? - Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0 - The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- [Better PDF support (Humble Bundle, Fanatical, etc)](https://github.com/ciromattia/kcc/issues/680)
- Huge margins / slow page turns? - Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB. - You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -253,11 +256,11 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file Output generated file to specified directory or file
-t TITLE, --title TITLE -t TITLE, --title TITLE
Comic title [Default=filename or directory name] Comic title [Default=filename or directory name]
--comicinfotitle Write title from ComicInfo.xml --metadatatitle Write title from ComicInfo.xml or other embedded metadata
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
-f FORMAT, --format FORMAT -f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto] Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
-b BATCHSPLIT, --batchsplit BATCHSPLIT -b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
@@ -403,7 +406,7 @@ Older links (dead):
## PRIVACY ## PRIVACY
**KCC** is initiating internet connections in two cases: **KCC** is initiating internet connections in two cases:
* During startup - Version check. * During startup - Version check and announcement check.
* When error occurs - Automatic reporting on Windows and macOS. * When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES ## KNOWN ISSUES
@@ -412,3 +415,6 @@ Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
## COPYRIGHT ## COPYRIGHT
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu. Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details. **KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
## Verification
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180

View File

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

View File

@@ -24,6 +24,12 @@
</property> </property>
<item row="2" column="0" colspan="2"> <item row="2" column="0" colspan="2">
<widget class="QListWidget" name="jobList"> <widget class="QListWidget" name="jobList">
<property name="minimumSize">
<size>
<width>0</width>
<height>150</height>
</size>
</property>
<property name="styleSheet"> <property name="styleSheet">
<string notr="true"/> <string notr="true"/>
</property> </property>
@@ -86,7 +92,7 @@
</property> </property>
<property name="icon"> <property name="icon">
<iconset resource="KCC.qrc"> <iconset resource="KCC.qrc">
<normaloff>:/Other/icons/kofi_symbol.png</normaloff>:/Other/icons/kofi_symbol.png</iconset> <normaloff>:/Brand/icons/kofi_symbol.png</normaloff>:/Other/icons/kofi_symbol.png</iconset>
</property> </property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
@@ -476,7 +482,7 @@
<item row="3" column="0"> <item row="3" column="0">
<widget class="QCheckBox" name="borderBox"> <widget class="QCheckBox" name="borderBox">
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be filled with white color.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Unchecked - Autodetection&lt;br/&gt;&lt;/span&gt;The color of margins fill will be detected automatically.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Indeterminate - White&lt;br/&gt;&lt;/span&gt;Margins will be untouched.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600; text-decoration: underline;&quot;&gt;Checked - Black&lt;br/&gt;&lt;/span&gt;Margins will be filled with black color.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>W/B margins</string> <string>W/B margins</string>
@@ -585,12 +591,12 @@
</widget> </widget>
</item> </item>
<item row="7" column="0"> <item row="7" column="0">
<widget class="QCheckBox" name="comicinfoTitleBox"> <widget class="QCheckBox" name="metadataTitleBox">
<property name="toolTip"> <property name="toolTip">
<string>Write Title from ComicInfo.xml</string> <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Write Title from ComicInfo.xml or other embedded metadata.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property> </property>
<property name="text"> <property name="text">
<string>ComicInfo Title</string> <string>Metadata Title</string>
</property> </property>
</widget> </widget>
</item> </item>

BIN
icons/Bindle_Red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
icons/Humble_H-Red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -17,6 +17,7 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
from datetime import datetime, timezone
import itertools import itertools
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
@@ -124,7 +125,7 @@ class Icons:
self.EPUBFormat = QIcon() self.EPUBFormat = QIcon()
self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off) self.EPUBFormat.addPixmap(QPixmap(":/Formats/icons/EPUB.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.KFXFormat = QIcon() self.KFXFormat = QIcon()
self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Normal, QIcon.Off) self.KFXFormat.addPixmap(QPixmap(":/Formats/icons/KFX.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.info = QIcon() self.info = QIcon()
self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off) self.info.addPixmap(QPixmap(":/Status/icons/info.png"), QIcon.Mode.Normal, QIcon.State.Off)
@@ -136,6 +137,15 @@ class Icons:
self.programIcon = QIcon() self.programIcon = QIcon()
self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off) self.programIcon.addPixmap(QPixmap(":/Icon/icons/comic2ebook.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.kofi = QIcon()
self.kofi.addPixmap(QPixmap(":/Brand/icons/kofi_symbol.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.humble = QIcon()
self.humble.addPixmap(QPixmap(":/Brand/icons/Humble_H-Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
self.bindle = QIcon()
self.bindle.addPixmap(QPixmap(":/Brand/icons/Bindle_Red.png"), QIcon.Mode.Normal, QIcon.State.Off)
class VersionThread(QThread): class VersionThread(QThread):
def __init__(self): def __init__(self):
@@ -150,19 +160,50 @@ class VersionThread(QThread):
def run(self): def run(self):
try: try:
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json() # unauthenticated API requests limit is 60 req/hour
if getattr(sys, 'frozen', False):
json_parser = requests.get("https://api.github.com/repos/ciromattia/kcc/releases/latest").json()
html_url = json_parser["html_url"] html_url = json_parser["html_url"]
latest_version = json_parser["tag_name"] latest_version = json_parser["tag_name"]
latest_version = re.sub(r'^v', "", latest_version) latest_version = re.sub(r'^v', "", latest_version)
if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \ if ("b" not in __version__ and Version(latest_version) > Version(__version__)) \
or ("b" in __version__ or ("b" in __version__
and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))): and Version(latest_version) >= Version(re.sub(r'b.*', '', __version__))):
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning', MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
False) False)
except Exception: except Exception:
return pass
try:
announcements = requests.get('https://api.github.com/repos/axu2/kcc-messages/contents/links.json',
headers={
'Accept': 'application/vnd.github.raw+json',
'X-GitHub-Api-Version': '2022-11-28'}).json()
for category, payloads in announcements.items():
for payload in payloads:
expiration = datetime.fromisoformat(payload['expiration'])
if expiration < datetime.now(timezone.utc):
continue
delta = expiration - datetime.now(timezone.utc)
time_left = f"{delta.days} day(s) left"
icon = 'info'
if category == 'humbleBundles':
icon = 'bindle'
if category == 'kofi':
icon = 'kofi'
message = f"<b>{payload.get('name')}</b>"
if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'):
message += f': {time_left}'
if category == 'humbleBundles':
message += ' [referral]'
MW.addMessage.emit(message, icon , False)
except Exception as e:
print(e)
def setAnswer(self, dialoganswer): def setAnswer(self, dialoganswer):
self.answer = dialoganswer self.answer = dialoganswer
@@ -249,11 +290,23 @@ class WorkerThread(QThread):
options.gamma = float(GUI.gammaValue) options.gamma = float(GUI.gammaValue)
if GUI.autoLevelBox.isChecked(): if GUI.autoLevelBox.isChecked():
options.autolevel = True options.autolevel = True
options.cropping = GUI.croppingBox.checkState().value if GUI.croppingBox.isChecked():
if GUI.croppingBox.checkState() == Qt.CheckState.PartiallyChecked:
options.cropping = 1
else:
options.cropping = 2
else:
options.cropping = 0
if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked: if GUI.croppingBox.checkState() != Qt.CheckState.Unchecked:
options.croppingp = float(GUI.croppingPowerValue) options.croppingp = float(GUI.croppingPowerValue)
options.preservemargin = GUI.preserveMarginBox.value() options.preservemargin = GUI.preserveMarginBox.value()
options.interpanelcrop = GUI.interPanelCropBox.checkState().value if GUI.interPanelCropBox.isChecked():
if GUI.interPanelCropBox.checkState() == Qt.CheckState.PartiallyChecked:
options.interpanelcrop = 1
else:
options.interpanelcrop = 2
else:
options.interpanelcrop = 0
if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.borderBox.checkState() == Qt.CheckState.PartiallyChecked:
options.white_borders = True options.white_borders = True
elif GUI.borderBox.checkState() == Qt.CheckState.Checked: elif GUI.borderBox.checkState() == Qt.CheckState.Checked:
@@ -268,8 +321,8 @@ class WorkerThread(QThread):
options.maximizestrips = True options.maximizestrips = True
if GUI.disableProcessingBox.isChecked(): if GUI.disableProcessingBox.isChecked():
options.noprocessing = True options.noprocessing = True
if GUI.comicinfoTitleBox.isChecked(): if GUI.metadataTitleBox.isChecked():
options.comicinfotitle = True options.metadatatitle = True
if GUI.deleteBox.isChecked(): if GUI.deleteBox.isChecked():
options.delete = True options.delete = True
if GUI.spreadShiftBox.isChecked(): if GUI.spreadShiftBox.isChecked():
@@ -324,6 +377,9 @@ class WorkerThread(QThread):
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False) MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files' GUI.progress.content = 'Creating CBZ files'
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files', 'info', False)
GUI.progress.content = 'Creating PDF files'
else: else:
MW.addMessage.emit('Creating EPUB files', 'info', False) MW.addMessage.emit('Creating EPUB files', 'info', False)
GUI.progress.content = 'Creating EPUB files' GUI.progress.content = 'Creating EPUB files'
@@ -368,6 +424,8 @@ class WorkerThread(QThread):
GUI.progress.content = '' GUI.progress.content = ''
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else: else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format: if 'MOBI' in gui_current_format:
@@ -464,7 +522,7 @@ class WorkerThread(QThread):
if os.path.isfile(path): if os.path.isfile(path):
os.remove(path) os.remove(path)
elif os.path.isdir(path): elif os.path.isdir(path):
rmtree(path) rmtree(path, True)
GUI.progress.content = '' GUI.progress.content = ''
GUI.progress.stop() GUI.progress.stop()
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
@@ -861,35 +919,35 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.settings.setValue('currentFormat', GUI.formatBox.currentIndex()) self.settings.setValue('currentFormat', GUI.formatBox.currentIndex())
self.settings.setValue('startNumber', self.startNumber + 1) self.settings.setValue('startNumber', self.startNumber + 1)
self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height())) self.settings.setValue('windowSize', str(MW.size().width()) + 'x' + str(MW.size().height()))
self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState().value, self.settings.setValue('options', {'mangaBox': GUI.mangaBox.checkState(),
'rotateBox': GUI.rotateBox.checkState().value, 'rotateBox': GUI.rotateBox.checkState(),
'qualityBox': GUI.qualityBox.checkState().value, 'qualityBox': GUI.qualityBox.checkState(),
'gammaBox': GUI.gammaBox.checkState().value, 'gammaBox': GUI.gammaBox.checkState(),
'autoLevelBox': GUI.autoLevelBox.checkState().value, 'autoLevelBox': GUI.autoLevelBox.checkState(),
'croppingBox': GUI.croppingBox.checkState().value, 'croppingBox': GUI.croppingBox.checkState(),
'croppingPowerSlider': float(self.croppingPowerValue) * 100, 'croppingPowerSlider': float(self.croppingPowerValue) * 100,
'preserveMarginBox': self.preserveMarginBox.value(), 'preserveMarginBox': self.preserveMarginBox.value(),
'interPanelCropBox': GUI.interPanelCropBox.checkState().value, 'interPanelCropBox': GUI.interPanelCropBox.checkState(),
'upscaleBox': GUI.upscaleBox.checkState().value, 'upscaleBox': GUI.upscaleBox.checkState(),
'borderBox': GUI.borderBox.checkState().value, 'borderBox': GUI.borderBox.checkState(),
'webtoonBox': GUI.webtoonBox.checkState().value, 'webtoonBox': GUI.webtoonBox.checkState(),
'outputSplit': GUI.outputSplit.checkState().value, 'outputSplit': GUI.outputSplit.checkState(),
'colorBox': GUI.colorBox.checkState().value, 'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState().value, 'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState().value, 'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'comicinfoTitleBox': GUI.comicinfoTitleBox.checkState().value, 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState().value, 'mozJpegBox': GUI.mozJpegBox.checkState(),
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(), 'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState().value, 'deleteBox': GUI.deleteBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState().value, 'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState().value, 'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState().value, 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState().value, 'noRotateBox': GUI.noRotateBox.checkState(),
'rotateFirstBox': GUI.rotateFirstBox.checkState().value, 'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState().value, 'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100, 'gammaSlider': float(self.gammaValue) * 100,
'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState().value, 'chunkSizeCheckBox': GUI.chunkSizeCheckBox.checkState(),
'chunkSizeBox': GUI.chunkSizeBox.value()}) 'chunkSizeBox': GUI.chunkSizeBox.value()})
self.settings.sync() self.settings.sync()
self.tray.hide() self.tray.hide()
@@ -964,7 +1022,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.setupUi(MW) self.setupUi(MW)
self.editor = KCCGUI_MetaEditor() self.editor = KCCGUI_MetaEditor()
self.icons = Icons() self.icons = Icons()
self.settings = QSettings('ciromattia', 'kcc') self.settings = QSettings('ciromattia', 'kcc9')
self.settingsVersion = self.settings.value('settingsVersion', '', type=str) self.settingsVersion = self.settings.value('settingsVersion', '', type=str)
self.lastPath = self.settings.value('lastPath', '', type=str) self.lastPath = self.settings.value('lastPath', '', type=str)
self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str)) self.defaultOutputFolder = str(self.settings.value('defaultOutputFolder', '', type=str))
@@ -974,7 +1032,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
self.currentFormat = self.settings.value('currentFormat', 0, type=int) self.currentFormat = self.settings.value('currentFormat', 0, type=int)
self.startNumber = self.settings.value('startNumber', 0, type=int) self.startNumber = self.settings.value('startNumber', 0, type=int)
self.windowSize = self.settings.value('windowSize', '0x0', type=str) self.windowSize = self.settings.value('windowSize', '0x0', type=str)
self.options = self.settings.value('options', {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}) default_options = {'gammaSlider': 0, 'croppingBox': 2, 'croppingPowerSlider': 100}
try:
self.options = self.settings.value('options', default_options)
except Exception:
self.options = default_options
self.worker = WorkerThread() self.worker = WorkerThread()
self.versionCheck = VersionThread() self.versionCheck = VersionThread()
self.progress = ProgressThread() self.progress = ProgressThread()
@@ -1011,6 +1073,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'}, "MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'}, "EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'}, "CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'}, "KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'}, "MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}, "EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1092,11 +1155,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'KoS'}, 'Label': 'KoS'},
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoE'}, 'Label': 'KoE'},
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk1'}, 'Label': 'Rmk1'},
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk2'}, 'Label': 'Rmk2'},
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True, "reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'RmkPP'}, 'Label': 'RmkPP'},
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False, "Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
'Label': 'OTHER'}, 'Label': 'OTHER'},
@@ -1163,7 +1226,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
statusBarLabel.setOpenExternalLinks(True) statusBarLabel.setOpenExternalLinks(True)
GUI.statusBar.addPermanentWidget(statusBarLabel, 1) GUI.statusBar.addPermanentWidget(statusBarLabel, 1)
self.addMessage('<b>Welcome!</b>', 'info')
self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info') self.addMessage('<b>Tip:</b> Hover mouse over options to see additional information in tooltips.', 'info')
self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info') self.addMessage('<b>Tip:</b> You can drag and drop image folders or comic files/archives into this window to convert.', 'info')
if self.startNumber < 5: if self.startNumber < 5:

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,7 @@ class Ui_mainWindow(object):
self.gridLayout.setContentsMargins(-1, -1, -1, 5) self.gridLayout.setContentsMargins(-1, -1, -1, 5)
self.jobList = QListWidget(self.centralWidget) self.jobList = QListWidget(self.centralWidget)
self.jobList.setObjectName(u"jobList") self.jobList.setObjectName(u"jobList")
self.jobList.setMinimumSize(QSize(0, 150))
self.jobList.setStyleSheet(u"") self.jobList.setStyleSheet(u"")
self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection) self.jobList.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.jobList.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
@@ -62,7 +63,7 @@ class Ui_mainWindow(object):
self.kofiButton.setObjectName(u"kofiButton") self.kofiButton.setObjectName(u"kofiButton")
self.kofiButton.setMinimumSize(QSize(0, 30)) self.kofiButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon() icon2 = QIcon()
icon2.addFile(u":/Other/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon2.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.kofiButton.setIcon(icon2) self.kofiButton.setIcon(icon2)
self.kofiButton.setIconSize(QSize(19, 16)) self.kofiButton.setIconSize(QSize(19, 16))
@@ -316,10 +317,10 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1) self.gridLayout_2.addWidget(self.outputSplit, 3, 1, 1, 1)
self.comicinfoTitleBox = QCheckBox(self.optionWidget) self.metadataTitleBox = QCheckBox(self.optionWidget)
self.comicinfoTitleBox.setObjectName(u"comicinfoTitleBox") self.metadataTitleBox.setObjectName(u"metadataTitleBox")
self.gridLayout_2.addWidget(self.comicinfoTitleBox, 7, 0, 1, 1) self.gridLayout_2.addWidget(self.metadataTitleBox, 7, 0, 1, 1)
self.qualityBox = QCheckBox(self.optionWidget) self.qualityBox = QCheckBox(self.optionWidget)
self.qualityBox.setObjectName(u"qualityBox") self.qualityBox.setObjectName(u"qualityBox")
@@ -546,7 +547,7 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None)) self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be filled with white color.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None)) self.borderBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - Autodetection<br/></span>The color of margins fill will be detected automatically.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - White<br/></span>Margins will be untouched.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - Black<br/></span>Margins will be filled with black color.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None)) self.borderBox.setText(QCoreApplication.translate("mainWindow", u"W/B margins", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
@@ -582,9 +583,9 @@ class Ui_mainWindow(object):
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None)) self.outputSplit.setText(QCoreApplication.translate("mainWindow", u"Output split", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.comicinfoTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"Write Title from ComicInfo.xml", None)) self.metadataTitleBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Write Title from ComicInfo.xml or other embedded metadata.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.comicinfoTitleBox.setText(QCoreApplication.translate("mainWindow", u"ComicInfo Title", None)) self.metadataTitleBox.setText(QCoreApplication.translate("mainWindow", u"Metadata Title", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None)) self.qualityBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style='white-space:pre'><span style=\" font-weight:600; text-decoration: underline;\">Indeterminate - 2 panels<br/></span>Zoom only the top and bottom of the page.</p><p><span style=\" font-weight:600; text-decoration: underline;\">Checked - 4 high-quality panels<br/></span>Zoom each corner separately. Try to increase the quality of magnification. Check wiki for more details.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)

View File

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

View File

@@ -44,7 +44,7 @@ from html import escape as hescape
import pymupdf import pymupdf
import numpy as np import numpy as np
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .comicarchive import SEVENZIP, available_archive_tools from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel from . import comic2panel
from . import image from . import image
@@ -126,9 +126,10 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
"</head>\n", "</head>\n",
"<body style=\"" + additionalStyle + "\">\n", "<body style=\"" + additionalStyle + "\">\n",
"<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n", "<div style=\"text-align:center;top:" + getTopMargin(deviceres, imgsizeframe) + "%;\">\n",
# this display none div fixes formatting issues with virtual panel mode, for some reason
'<div style="display:none;">.</div>\n',
]) ])
if options.iskindle:
# this display none div fixes formatting issues with virtual panel mode, for some reason
f.write('<div style="display:none;">.</div>\n')
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n') f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
if imgfile2: if imgfile2:
f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n') f.write(f'<img width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
@@ -277,7 +278,7 @@ def buildNAV(dstdir, title, chapters, chapternames):
f.close() f.close()
def buildOPF(dstdir, title, filelist, cover=None): def buildOPF(dstdir, title, filelist, originalpath, cover=None):
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf') opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
deviceres = options.profileData[1] deviceres = options.profileData[1]
if options.righttoleft: if options.righttoleft:
@@ -365,6 +366,11 @@ def buildOPF(dstdir, title, filelist, cover=None):
else: else:
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n") f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
pageside = "left" pageside = "left"
if originalpath.lower().endswith('.pdf'):
if pageside == "right":
pageside = "left"
else:
pageside = "right"
if options.spreadshift: if options.spreadshift:
if pageside == "right": if pageside == "right":
pageside = "left" pageside = "left"
@@ -439,7 +445,7 @@ def buildOPF(dstdir, title, filelist, cover=None):
"</container>"]) "</container>"])
f.close() f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len_tomes=0): def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, len_tomes=0):
filelist = [] filelist = []
chapterlist = [] chapterlist = []
os.mkdir(os.path.join(path, 'OEBPS', 'Text')) os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
@@ -527,6 +533,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
f.close() f.close()
build_html_start = perf_counter() build_html_start = perf_counter()
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes) cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
dot_clean(path)
options.covers.append((cover, options.uuid)) options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')): for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
chapter = False chapter = False
@@ -578,7 +585,36 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
chapternames[filename] = aChapter[1] chapternames[filename] = aChapter[1]
buildNCX(path, options.title, chapterlist, chapternames) buildNCX(path, options.title, chapterlist, chapternames)
buildNAV(path, options.title, chapterlist, chapternames) buildNAV(path, options.title, chapterlist, chapternames)
buildOPF(path, options.title, filelist, cover) buildOPF(path, options.title, filelist, originalpath, cover)
def buildPDF(path, title, cover=None, output_file=None):
"""
Build a PDF file from processed comic images.
Images are combined into a single PDF optimized for e-readers.
"""
start = perf_counter()
# open empty PDF
with pymupdf.open() as doc:
doc.set_metadata({'title': title, 'author': options.authors[0]})
# Stream images to PDF
for root, dirs, files in os.walk(os.path.join(path, "OEBPS", "Images")):
files.sort(key=OS_SORT_KEY)
dirs.sort(key=OS_SORT_KEY)
for file in files:
w, h = Image.open(os.path.join(root, file)).size
page = doc.new_page(width=w, height=h)
page.insert_image(page.rect, filename=os.path.join(root, file))
# determine output filename if not provided
if output_file is None:
output_file = getOutputFilename(path, None, '.pdf', '')
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
print(f"MuPDF output: {end-start} sec")
return output_file
def imgDirectoryProcessing(path): def imgDirectoryProcessing(path):
@@ -658,7 +694,8 @@ def imgFileProcessing(work):
pass pass
elif opt.forcepng: elif opt.forcepng:
img.convertToGrayscale() img.convertToGrayscale()
img.quantizeImage() if opt.format != 'PDF':
img.quantizeImage()
else: else:
img.convertToGrayscale() img.convertToGrayscale()
output.append(img.saveToDir()) output.append(img.saveToDir())
@@ -753,16 +790,16 @@ def extract_page(vector):
width, height = int(page.rect.width), int(page.rect.height) width, height = int(page.rect.width), int(page.rect.height)
blank_page = Image.new("RGB", (width, height), "white") blank_page = Image.new("RGB", (width, height), "white")
blank_page.save(output_path) blank_page.save(output_path)
xref = image_list[0][0]
d = doc.extract_image(xref)
if d['cs-name'] == 'DeviceCMYK':
pix = pymupdf.Pixmap(doc, xref)
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
pix.save(output_path)
else: else:
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout: xref = image_list[0][0]
imgout.write(d["image"]) d = doc.extract_image(xref)
if d['cs-name'] == 'DeviceCMYK':
pix = pymupdf.Pixmap(doc, xref)
pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
pix.save(output_path)
else:
with open(Path(output_path).with_suffix('.' + d['ext']), "wb") as imgout:
imgout.write(d["image"])
print("Processed page numbers %i through %i" % (seg_from, seg_to - 1)) print("Processed page numbers %i through %i" % (seg_from, seg_to - 1))
@@ -778,6 +815,11 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
if len(page.get_images()) > 1: if len(page.get_images()) > 1:
render = True render = True
break break
if len(page.get_images()) == 1:
image = page.get_images()[0]
if not image[5] or image[8] == 'CCITTFaxDecode':
render = True
break
cpu = cpu_count() cpu = cpu_count()
@@ -890,7 +932,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
return filename return filename
def getComicInfo(path, originalpath): def getMetadata(path, originalpath):
xmlPath = os.path.join(path, 'ComicInfo.xml') xmlPath = os.path.join(path, 'ComicInfo.xml')
options.comicinfo_chapters = [] options.comicinfo_chapters = []
options.summary = '' options.summary = ''
@@ -909,13 +951,14 @@ def getComicInfo(path, originalpath):
else: else:
defaultAuthor = False defaultAuthor = False
options.authors = [options.author] options.authors = [options.author]
if os.path.exists(xmlPath): if os.path.exists(xmlPath):
try: try:
xml = metadata.MetadataParser(xmlPath) xml = metadata.MetadataParser(xmlPath)
except Exception: except Exception:
os.remove(xmlPath) os.remove(xmlPath)
return return
if options.comicinfotitle: if options.metadatatitle:
options.title = xml.data['Title'] options.title = xml.data['Title']
elif defaultTitle: elif defaultTitle:
if xml.data['Series']: if xml.data['Series']:
@@ -941,6 +984,13 @@ def getComicInfo(path, originalpath):
options.summary = xml.data['Summary'] options.summary = xml.data['Summary']
os.remove(xmlPath) os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'):
with pymupdf.open(originalpath) as doc:
if options.metadatatitle and doc.metadata['title']:
options.title = doc.metadata['title']
if defaultAuthor and doc.metadata['author']:
options.authors = [doc.metadata['author']]
def getDirectorySize(start_path='.'): def getDirectorySize(start_path='.'):
total_size = 0 total_size = 0
@@ -1040,19 +1090,12 @@ def sanitizePermissions(filetree):
dot_clean(filetree) dot_clean(filetree)
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
for name in files:
if name.startswith('._'):
os.remove(os.path.join(root, name))
def chunk_directory(path): def chunk_directory(path):
level = -1 level = -1
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')): for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
for f in files: for f in files:
# Windows MAX_LENGTH = 260 plus some buffer # Windows MAX_LEN = 260 plus some buffer
if len(os.path.join(root, f)) > 180: if os.name == 'nt' and len(os.path.join(root, f)) > 180:
flattenTree(os.path.join(path, 'OEBPS', 'Images')) flattenTree(os.path.join(path, 'OEBPS', 'Images'))
level = 1 level = 1
break break
@@ -1143,6 +1186,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
try: try:
img = Image.open(path) img = Image.open(path)
imageNumber += 1 imageNumber += 1
# count images smaller than device resolution
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]: if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1 imageSmaller += 1
except Exception as err: except Exception as err:
@@ -1190,7 +1234,6 @@ def slugify(value, is_natural_sorted):
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2)) value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value return value
def makeZIP(zipfilename, basedir, isepub=False): def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip' zipfilename = os.path.abspath(zipfilename) + '.zip'
@@ -1215,7 +1258,6 @@ def makeZIP(zipfilename, basedir, isepub=False):
print(f"makeZIP time: {end - start} seconds") print(f"makeZIP time: {end - start} seconds")
return zipfilename return zipfilename
def makeParser(): def makeParser():
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False) psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
@@ -1248,12 +1290,12 @@ def makeParser():
help="Output generated file to specified directory or file") help="Output generated file to specified directory or file")
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle", output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Comic title [Default=filename or directory name]") help="Comic title [Default=filename or directory name]")
output_options.add_argument("--comicinfotitle", action="store_true", dest="comicinfotitle", default=False, output_options.add_argument("--metadatatitle", action="store_true", dest="metadatatitle", default=False,
help="Write Title from ComicInfo.xml") help="Write Title from ComicInfo.xml or other embedded metadata")
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor", output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]") help="Author name [Default=KCC]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto", output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) " help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]") "[Default=Auto]")
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False, output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'") help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
@@ -1343,6 +1385,8 @@ def checkOptions(options):
options.format = 'CBZ' options.format = 'CBZ'
elif options.profile in image.ProfileData.ProfilesKindle.keys(): elif options.profile in image.ProfileData.ProfilesKindle.keys():
options.format = 'MOBI' options.format = 'MOBI'
elif options.profile in image.ProfileData.ProfilesRemarkable.keys():
options.format = 'PDF'
else: else:
options.format = 'EPUB' options.format = 'EPUB'
if options.profile in image.ProfileData.ProfilesKindle.keys(): if options.profile in image.ProfileData.ProfilesKindle.keys():
@@ -1479,7 +1523,7 @@ def makeBook(source, qtgui=None):
print("Preparing source images...") print("Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source)
print("Checking images...") print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source) getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images")) removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
@@ -1497,7 +1541,7 @@ def makeBook(source, qtgui=None):
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images")) imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI: if GUI:
GUI.progressBarTick.emit('1') GUI.progressBarTick.emit('1')
if options.batchsplit > 0: if options.batchsplit > 0 or options.targetsize:
tomes = chunk_directory(path) tomes = chunk_directory(path)
else: else:
tomes = [path] tomes = [path]
@@ -1506,6 +1550,8 @@ def makeBook(source, qtgui=None):
if GUI: if GUI:
if options.format == 'CBZ': if options.format == 'CBZ':
GUI.progressBarTick.emit('Compressing CBZ files') GUI.progressBarTick.emit('Compressing CBZ files')
elif options.format == 'PDF':
GUI.progressBarTick.emit('Creating PDF files')
else: else:
GUI.progressBarTick.emit('Compressing EPUB files') GUI.progressBarTick.emit('Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1)) GUI.progressBarTick.emit(str(len(tomes) + 1))
@@ -1527,21 +1573,31 @@ def makeBook(source, qtgui=None):
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images")) makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
elif options.format == 'PDF':
print("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)
filepath.append(output_pdf)
else: else:
print("Creating EPUB file...") print("Creating EPUB file...")
if len(tomes) > 1: if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True, cover, len(tomes)) buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber))) filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else: else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover) buildEPUB(tome, chapterNames, tomeNumber, False, cover, source)
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True) makeZIP(tome + '_comic', tome, True)
copyfile(tome + '_comic.zip', filepath[-1]) # Copy files to final destination (PDF files are already saved directly)
try: if options.format != 'PDF':
os.remove(tome + '_comic.zip') copyfile(tome + '_comic.zip', filepath[-1])
except FileNotFoundError: try:
# newly temporary created file is not found. It might have been already deleted os.remove(tome + '_comic.zip')
pass except FileNotFoundError:
# newly temporary created file is not found. It might have been already deleted
pass
rmtree(tome, True) rmtree(tome, True)
if GUI: if GUI:
GUI.progressBarTick.emit('tick') GUI.progressBarTick.emit('tick')
@@ -1572,10 +1628,15 @@ def makeBook(source, qtgui=None):
if os.path.isfile(source): if os.path.isfile(source):
os.remove(source) os.remove(source)
elif os.path.isdir(source): elif os.path.isdir(source):
rmtree(source) rmtree(source, True)
end = perf_counter() end = perf_counter()
print(f"makeBook: {end - start} seconds") print(f"makeBook: {end - start} seconds")
# Clean up temporary workspace
try:
rmtree(path, True)
except Exception:
pass
return filepath return filepath
@@ -1655,3 +1716,4 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close() makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join() makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput return makeMOBIWorkerOutput

View File

@@ -24,7 +24,7 @@ from argparse import ArgumentParser
from shutil import rmtree, copytree, move from shutil import rmtree, copytree, move
from multiprocessing import Pool from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw from PIL import Image, ImageChops, ImageOps, ImageDraw
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
def mergeDirectoryTick(output): def mergeDirectoryTick(output):
@@ -44,6 +44,7 @@ def mergeDirectory(work):
imagesValid = [] imagesValid = []
sizes = [] sizes = []
targetHeight = 0 targetHeight = 0
dot_clean(directory)
for root, _, files in walkLevel(directory, 0): for root, _, files in walkLevel(directory, 0):
for name in files: for name in files:
if getImageFileName(name) is not None: if getImageFileName(name) is not None:
@@ -253,6 +254,7 @@ def main(argv=None, qtgui=None):
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1]) mergeWorkerOutput[0][1])
print("Splitting images...") print("Splitting images...")
dot_clean(targetDir)
for root, _, files in os.walk(targetDir, False): for root, _, files in os.walk(targetDir, False):
for name in files: for name in files:
if getImageFileName(name) is not None: if getImageFileName(name) is not None:
@@ -277,7 +279,7 @@ def main(argv=None, qtgui=None):
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0], raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
splitWorkerOutput[0][1]) splitWorkerOutput[0][1])
if args.inPlace: if args.inPlace:
rmtree(sourceDir) rmtree(sourceDir, True)
move(targetDir, sourceDir) move(targetDir, sourceDir)
else: else:
rmtree(targetDir, True) rmtree(targetDir, True)

View File

@@ -383,7 +383,7 @@ class ComicPage:
def optimizeForDisplay(self, eraserainbow, is_color): def optimizeForDisplay(self, eraserainbow, is_color):
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array # Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
if eraserainbow: if eraserainbow and all(dim > 1 for dim in self.image.size):
self.image = erase_rainbow_artifacts(self.image, is_color) self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self): def resizeImage(self):
@@ -397,7 +397,7 @@ class ComicPage:
else: # if image bigger than device resolution or smaller with upscaling else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD: if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method) self.image = ImageOps.fit(self.image, self.size, method=method)
elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders: elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill) self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else: else:
self.image = ImageOps.contain(self.image, self.size, method=method) self.image = ImageOps.contain(self.image, self.size, method=method)
@@ -423,12 +423,20 @@ class ComicPage:
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill) bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
if bbox: if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropMargin(self, power, minimum): def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.fill) bbox = get_bbox_crop_margin(self.image, power, self.fill)
if bbox: if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum) self.maybeCrop(bbox, minimum)
def cropInterPanelEmptySections(self, direction): def cropInterPanelEmptySections(self, direction):
@@ -463,7 +471,7 @@ class Cover:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h)) self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h)) self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.3: elif w / h > 1.34:
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h)) self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else: else:
@@ -487,9 +495,6 @@ class Cover:
stroke_width=25 stroke_width=25
) )
copy.save(target, "JPEG", optimize=1, quality=85) copy.save(target, "JPEG", optimize=1, quality=85)
dot_cover = Path(target).with_stem('._' + Path(target).stem)
if os.path.exists(dot_cover):
os.remove(dot_cover)
except IOError: except IOError:
raise RuntimeError('Failed to save cover.') raise RuntimeError('Failed to save cover.')

View File

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

View File

@@ -52,6 +52,7 @@ def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
''' '''
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
bw_bbox = bw_img.getbbox() bw_bbox = bw_img.getbbox()
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black. if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
return None return None
@@ -141,9 +142,26 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
''' '''
threshold = threshold_from_power(power) threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0) bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
return bw_img.getbbox() return bw_img.getbbox()
def ignore_pixels_near_edge(bw_img):
w, h = bw_img.size
edge_bbox = [
(0, 0, w, int(0.02 * h)),
(0, int(0.98 * h), w, h),
(0, 0, int(0.02 * w), h),
(int(0.98 * w), 0, w, h)
]
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)
def box_intersect(box1, box2, max_dist): def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1] return not (box2[0]-max_dist[0] > box1[1]

View File

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

View File

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

12
requirements-win7.txt Normal file
View File

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

View File

@@ -9,4 +9,4 @@ mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.8.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.26.1 PyMuPDF>=1.18.0

View File

@@ -38,10 +38,17 @@ class BuildBinaryCommand(setuptools.Command):
if sys.platform == 'darwin': if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py') os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v # TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg') min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET')
if min_os:
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
else:
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
sys.exit(0) sys.exit(0)
elif sys.platform == 'win32': elif sys.platform == 'win32':
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py') if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_' + VERSION + ' -w --noupx kcc.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
sys.exit(0) sys.exit(0)
elif sys.platform == 'linux': elif sys.platform == 'linux':
os.system( os.system(
@@ -74,8 +81,9 @@ setuptools.setup(
}, },
packages=['kindlecomicconverter'], packages=['kindlecomicconverter'],
install_requires=[ install_requires=[
'pyside6>=6.5.1', 'pyside6>=6.0.0',
'Pillow>=11.3.0', 'Pillow>=9.3.0',
'PyMuPDF>=1.18.0',
'psutil>=5.9.5', 'psutil>=5.9.5',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0', 'raven>=6.0.0',
@@ -84,7 +92,7 @@ setuptools.setup(
'natsort>=8.4.0', 'natsort>=8.4.0',
'distro', 'distro',
'numpy>=1.22.4', 'numpy>=1.22.4',
'PyMuPDF>=1.26.1', 'PyMuPDF>=1.16.1',
], ],
classifiers=[], classifiers=[],
zip_safe=False, zip_safe=False,