Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c6b8611a | ||
|
|
1b3b619491 | ||
|
|
fb784a2eb4 | ||
|
|
f2deb1061f | ||
|
|
3be76ec89f | ||
|
|
38fa13610f | ||
|
|
6d3bd0296c | ||
|
|
9decf0e68e | ||
|
|
dc2cc8e7b8 | ||
|
|
55468019dd | ||
|
|
8f191e604b | ||
|
|
924f83d976 | ||
|
|
52d52ff323 | ||
|
|
e93be5cf68 | ||
|
|
4cdbb4beef | ||
|
|
2c9056c524 | ||
|
|
a503f54f9f | ||
|
|
fc331d4e19 | ||
|
|
adac558aab | ||
|
|
c8cf50da0a | ||
|
|
f38f1c45b2 | ||
|
|
bd0328a12f | ||
|
|
846a755761 | ||
|
|
ff80d275e6 | ||
|
|
b3e1d393b6 | ||
|
|
b4c71b3b91 | ||
|
|
7e412f12ae | ||
|
|
2076775d94 | ||
|
|
59df22b62d | ||
|
|
ff5edc5ce5 | ||
|
|
a52c0ce03a | ||
|
|
1c0506fe7d | ||
|
|
806c6232c3 | ||
|
|
6f9f5067d8 | ||
|
|
11be0dffcc | ||
|
|
4c10569ab1 | ||
|
|
2a5cd6718d | ||
|
|
fd1468904f | ||
|
|
feec40ffc8 | ||
|
|
f6ae7661bc | ||
|
|
ca14a4c813 | ||
|
|
0c1071a358 | ||
|
|
858251f509 | ||
|
|
3c66aa9a67 | ||
|
|
077957e665 | ||
|
|
c3f86e29c8 | ||
|
|
72e1546c2c | ||
|
|
544e866b51 | ||
|
|
363a36a764 | ||
|
|
bece5290a3 | ||
|
|
cab3703217 | ||
|
|
3723bf9d52 | ||
|
|
2a6d61530f | ||
|
|
5396f1f9c4 | ||
|
|
f1b58c83d6 | ||
|
|
0f009755b1 | ||
|
|
ee5bd150e5 | ||
|
|
a55c0ddb08 | ||
|
|
799961407e | ||
|
|
bc28df1f53 | ||
|
|
a8b2c055bf | ||
|
|
afa9c7e7e6 | ||
|
|
1f1b9a37fa | ||
|
|
c4d9512cbc | ||
|
|
89fc6437dd | ||
|
|
1e57da08a9 | ||
|
|
b4ef37dbb9 | ||
|
|
b95cf6e179 | ||
|
|
08070cdd97 | ||
|
|
df3d174437 | ||
|
|
dc4475bcb0 | ||
|
|
b2e7fd3f5a | ||
|
|
4102643110 | ||
|
|
ca6b3b7611 | ||
|
|
6d5db71b5b | ||
|
|
87c6b7143a | ||
|
|
a5bd995a6b | ||
|
|
bcc69b0f05 | ||
|
|
f96adc5dc3 | ||
|
|
f54b06e058 | ||
|
|
7ceeb29fae | ||
|
|
c385ef7ae0 | ||
|
|
9827f11944 | ||
|
|
8030884148 | ||
|
|
949fb0acb4 | ||
|
|
84f69a0950 | ||
|
|
868a31cb00 | ||
|
|
1af24b394f | ||
|
|
401876da22 | ||
|
|
ffeaaeca19 | ||
|
|
b95bb12393 | ||
|
|
4a6e4622ed | ||
|
|
a491810810 | ||
|
|
75d0342fe1 | ||
|
|
60d41b25e4 | ||
|
|
0db788589d | ||
|
|
f42e6aea5c | ||
|
|
53ae057cbb | ||
|
|
d729839976 | ||
|
|
42a50ed670 | ||
|
|
e6ef7c1732 | ||
|
|
f2a806a42a | ||
|
|
8798d71bfa | ||
|
|
19ce14eeee | ||
|
|
5a1e2dafcb | ||
|
|
f149ae23f3 |
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -21,20 +21,20 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Set Release Date
|
||||
id: release_date
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||
# Always creates the "latest" tag
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
type=raw,value=${{ steps.release_date.outputs.release_date }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
context: .
|
||||
|
||||
@@ -25,9 +25,9 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -59,12 +59,12 @@ jobs:
|
||||
env:
|
||||
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: AppImage
|
||||
path: './*.AppImage*'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
@@ -30,9 +30,9 @@ jobs:
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: '14.0'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
# apply provisioning profile
|
||||
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install -g appdmg
|
||||
@@ -80,12 +80,12 @@ jobs:
|
||||
run: |
|
||||
python setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-os-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
PYTHON_VERSION: 3.11.9
|
||||
MACOSX_DEPLOYMENT_TARGET: '10.14'
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Get Python
|
||||
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
|
||||
- name: Install Python
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
pip3 install --upgrade pip pyinstaller certifi
|
||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||
./gen_ui_files.sh
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 16
|
||||
- run: npm install -g appdmg
|
||||
@@ -51,12 +51,12 @@ jobs:
|
||||
run: |
|
||||
python3 setup.py build_binary
|
||||
- name: upload build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: osx-build-${{ runner.arch }}
|
||||
path: dist/*.dmg
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
@@ -35,9 +35,9 @@ jobs:
|
||||
command: build_c2p
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.11
|
||||
cache: 'pip'
|
||||
@@ -53,12 +53,12 @@ jobs:
|
||||
python setup.py ${{ matrix.command }}
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
uses: signpath/github-action-submit-signing-request@v2.3
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
@@ -27,9 +27,9 @@ jobs:
|
||||
env:
|
||||
WINDOWS_7: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.8
|
||||
cache: 'pip'
|
||||
@@ -46,12 +46,12 @@ jobs:
|
||||
python setup.py build_binary
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
uses: signpath/github-action-submit-signing-request@v2.3
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
prerelease: true
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
setup.bat
|
||||
kindlecomicconverter/sentry.py
|
||||
other/windows/kindlegen.exe
|
||||
dist/
|
||||
build/
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<img src="header.jpg" alt="Header Image" width="400">
|
||||
Most manga conversion methods for ereaders add margins. But KCC enables true fullscreen from top to bottom.
|
||||
|
||||

|
||||
|
||||
(feat. Ya Boy Kongming! and Fire Force from a [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1) PDF source.)
|
||||
|
||||
# KCC
|
||||
|
||||
@@ -11,53 +15,77 @@
|
||||
like Kindle, Kobo, ReMarkable, and more.
|
||||
Pages display in fullscreen without margins,
|
||||
with proper fixed layout support.
|
||||
Supported input formats include JPG/PNG image files in folders, archives like CBZ, or PDFs.
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
|
||||
KCC runs on Windows, macOS, and Linux.
|
||||
|
||||
Supported input formats include JPG/PNG image files in folders, archives like CBZ/EPUB, or PDF.
|
||||
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, PDF, and folders of images.
|
||||
|
||||
The absolute highest quality source files are DRM-free PDFs from [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical.
|
||||
The PDFs are often x12000 resolution or better, while even the 10" Kindle Scribe is only x2480.
|
||||
|
||||
KCC's main goal is maximum image quality at significantly smaller file size. For example, KCC
|
||||
|
||||
1) can compress a 600 MB manga volume from Humble Bundle to 100 MB.
|
||||
This is mostly accomplished by downscaling to the native resolution of your specific device.
|
||||
This can also improve battery life, page turn speed, and general performance
|
||||
on underpowered ereaders with small memory and storage capacities.
|
||||
|
||||
2) fixes black levels to avoid gray/faded blacks found in many Kindle Store manga
|
||||
|
||||

|
||||
|
||||
3) fixes [the rainbow effect on Kaleido 3 color eink](https://www.youtube.com/watch?v=Dw2HTJCGMhw) without blur:
|
||||
|
||||

|
||||
|
||||
4) can [semi-automatically detect 2-page spreads](https://www.youtube.com/watch?v=kfIX67f7Aqk) from pre-split single pages:
|
||||
|
||||

|
||||
|
||||
You can view combined spreads either rotated or not:
|
||||
|
||||

|
||||
|
||||
You can view the split halves either before or after combined spreads:
|
||||
|
||||

|
||||
|
||||
## Demo
|
||||
|
||||
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
|
||||
|
||||
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
|
||||
|
||||
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
|
||||
Then all your usb loaded comic files will show up next to all your normal books in your ereader library!
|
||||
|
||||
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
|
||||
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
|
||||
for optimal compatibility with your device's native PDF reader.
|
||||
The final result looks incredible on my 10" Kindle Scribe: https://www.youtube.com/watch?v=2CIHW2N9Enc
|
||||
|
||||
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
|
||||
which can be directly converted by KCC.
|
||||
**WARNING**: Kindle Scribe 2025 MOBI support may have blank pages. Use PDF instead.
|
||||
|
||||
Its main feature is various optional image processing steps to look good on eink screens,
|
||||
which have different requirements than normal LCD screens.
|
||||
Combining that with downscaling to your specific device's screen resolution
|
||||
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
|
||||
This can also improve battery life, page turn speed, and general performance
|
||||
on underpowered ereaders with small memory and storage capacities.
|
||||
The GUI looks like this, built in Qt6.
|
||||
There are a lot of options, but most people only care about the ones checked below.
|
||||
Hover over each option to see details in a tooltip.
|
||||
|
||||
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
|
||||
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
|
||||
2) unneccessary margins at the bottom of the screen
|
||||
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
|
||||
4) incorrect page turn direction for manga that's read right to left
|
||||
5) unaligned two page spreads in landscape, where pages are shifted over by 1
|
||||
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
|
||||

|
||||
|
||||
The GUI looks like this, built in Qt6, with my most commonly used settings:
|
||||
|
||||

|
||||
|
||||
Simply drag and drop your files/folders into the KCC window,
|
||||
adjust your settings (hover over each option to see details in a tooltip),
|
||||
and hit convert to create ereader optimized files.
|
||||
You can change the default output directory by holding `Shift` while clicking the convert button.
|
||||
Then just drag and drop the generated output files onto your device's documents folder via USB.
|
||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
|
||||
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac or OpenMTP.
|
||||
|
||||
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
|
||||
|
||||
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
|
||||
|
||||
## Kindle Jailbreak for KOreader
|
||||
|
||||
Jailbreaking to use KOreader is optional. KCC works just fine with the native mobi manga viewer.
|
||||
|
||||
But if you jailbreak your Kindle, you'll unlock additional features like:
|
||||
|
||||
1) compatibility with image formats better suited for manga like 4-bit PNG and WEBP. This enables better quality at **half the file size** compared to JPG which must be 8-bit.
|
||||
This is visually lossless since eink screens are 4-bit.
|
||||
2) no dependency on kindlegen, which can mean **twice as fast conversions** since you can convert to CBZ directly.
|
||||
3) You can dither by partially checking the JPG/PNG box and can crop in KCC instead of KOreader. This may increase page turn speed and battery.
|
||||
|
||||
### A word of warning
|
||||
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
|
||||
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
|
||||
@@ -123,6 +151,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
||||
Going back a few pages and exiting and re-entering book should fix it temporarily.
|
||||
- What output format should I use?
|
||||
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
|
||||
- Where is KEPUB option?
|
||||
- Choosing a Kobo profile and EPUB output will output KEPUB.
|
||||
- All options have additional information in tooltips if you hover over the option.
|
||||
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
|
||||
- Kindle panel view not working?
|
||||
@@ -145,6 +175,8 @@ You'll need to install various tools to access important but optional features.
|
||||
|
||||
### KindleGen
|
||||
|
||||
(This is not needed for Koreader users on jailbroken Kindle. Just set output to CBZ instead.)
|
||||
|
||||
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
|
||||
|
||||
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
|
||||
@@ -199,6 +231,8 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
||||
'KCS': ("Kindle Colorsoft", (1272, 1696), 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),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), 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),
|
||||
@@ -237,18 +271,23 @@ MAIN:
|
||||
Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoCC, KoL, KoLC, KoF, KoS, KoE)
|
||||
[Default=KV]
|
||||
-m, --manga-style Manga style (right-to-left reading and splitting)
|
||||
--lightnovel Only resize images and preserve original file structure.
|
||||
--ebok Force EBOK tag instead of PDOC for MOBI
|
||||
--invertdirection Invert page turn direction
|
||||
-q, --hq Try to increase the quality of magnification
|
||||
-2, --two-panel Display two not four panels in Panel View mode
|
||||
--vertical4panel Show side panels first in virtual panel view
|
||||
-w, --webtoon Webtoon processing mode
|
||||
--ts TARGETSIZE, --targetsize TARGETSIZE
|
||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||
|
||||
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.
|
||||
--legacyextract Use legacy PDF/EPUB image extraction method from earlier KCC versions.
|
||||
--pdfwidth Render vector PDFs based on device width instead of height.
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
--wallpaper Crop images to to fill screen
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||
-g GAMMA, --gamma GAMMA
|
||||
@@ -267,7 +306,7 @@ 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
|
||||
--nosmartcovercrop Disable attempt to crop main cover from wide image
|
||||
--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 for black and white images
|
||||
@@ -287,14 +326,17 @@ OUTPUT SETTINGS:
|
||||
-t TITLE, --title TITLE
|
||||
Comic title [Default=filename or directory name]
|
||||
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
|
||||
--keepcomicinfo Keep any original ComicInfo.xml files [Default=0]
|
||||
-a AUTHOR, --author AUTHOR
|
||||
Author name [Default=KCC]
|
||||
--language EPUB language [Default=en-US]
|
||||
-f FORMAT, --format FORMAT
|
||||
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
|
||||
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
|
||||
-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]
|
||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||
--onepagelandscape Show a single centered page in landscape
|
||||
--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.
|
||||
@@ -362,6 +404,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python -m venv venv
|
||||
venv\Scripts\activate.bat
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
@@ -387,6 +430,7 @@ One time setup and running for the first time:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
python kcc.py
|
||||
```
|
||||
@@ -440,7 +484,6 @@ Older links (dead):
|
||||
## PRIVACY
|
||||
**KCC** is initiating internet connections in two cases:
|
||||
* During startup - Version check and announcement check.
|
||||
* When error occurs - Automatic reporting on Windows and macOS.
|
||||
|
||||
## KNOWN ISSUES
|
||||
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
|
||||
|
||||
@@ -62,6 +62,19 @@
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="volumeLine"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QCheckBox" name="bulkVolumeCheck">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string><b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> → sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> → range from 1 to 10<br><code>1, 3, 5</code> → specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
@@ -195,6 +208,7 @@
|
||||
<tabstops>
|
||||
<tabstop>seriesLine</tabstop>
|
||||
<tabstop>volumeLine</tabstop>
|
||||
<tabstop>bulkVolumeCheck</tabstop>
|
||||
<tabstop>titleLine</tabstop>
|
||||
<tabstop>numberLine</tabstop>
|
||||
<tabstop>writerLine</tabstop>
|
||||
|
||||
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 921 KiB After Width: | Height: | Size: 921 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 224 KiB |
|
After Width: | Height: | Size: 358 KiB |
|
After Width: | Height: | Size: 513 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 181 KiB |
@@ -0,0 +1,61 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# macOS GUI build, used by setup.py build_binary
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, SPECPATH)
|
||||
from kindlecomicconverter import __version__
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['kcc.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=['_cffi_backend'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='Kindle Comic Converter',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['icons/comic2ebook.icns'],
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
strip=True,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='Kindle Comic Converter',
|
||||
)
|
||||
app = BUNDLE(
|
||||
coll,
|
||||
name='Kindle Comic Converter.app',
|
||||
icon='icons/comic2ebook.icns',
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'CFBundleShortVersionString': __version__,
|
||||
'CFBundleVersion': __version__,
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import (Qt)
|
||||
from PySide6.QtGui import (QKeyEvent, QPixmap)
|
||||
from PySide6.QtWidgets import (QHBoxLayout, QLabel, QDialog)
|
||||
|
||||
class LabelSpreadsDialog(QDialog):
|
||||
def __init__(self, available_height, images, spreads):
|
||||
super().__init__()
|
||||
self.index = 0
|
||||
self.images = images
|
||||
self.spreads = spreads
|
||||
self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
|
||||
|
||||
self.setWindowTitle("TODO: Filename goes here")
|
||||
# self.setGeometry(APP.primaryScreen().availableGeometry())
|
||||
# self.setMaximumSize(APP.primaryScreen().availableSize())
|
||||
self.available_height = available_height
|
||||
|
||||
layout = QHBoxLayout()
|
||||
self.setLayout(layout)
|
||||
|
||||
label = QLabel()
|
||||
label2 = QLabel()
|
||||
self.label = label
|
||||
self.label2 = label2
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(label2)
|
||||
label2.setText("not a spread")
|
||||
|
||||
help_text = [
|
||||
"Use arrows to change index.",
|
||||
"Use space bar to confirm spreads.",
|
||||
"Use spread shift option to offset by 1.",
|
||||
"Use enter key to confirm all spreads."
|
||||
"Close window to cancel."
|
||||
]
|
||||
buttonLabel = QLabel('\n'.join(help_text))
|
||||
layout.addWidget(buttonLabel)
|
||||
# print(label.size())
|
||||
# print(label.maximumSize())
|
||||
# l, t, r, b = layout.getContentsMargins()
|
||||
|
||||
#label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
|
||||
label.setPixmap(pixmap)
|
||||
#label.setScaledContents(True)
|
||||
|
||||
#label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
# pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# label2.setPixmap(pixmap2)
|
||||
#label2.setScaledContents(True)
|
||||
#self.resize(pixmap2.width(), pixmap2.height())
|
||||
|
||||
def keyReleaseEvent(self, event):
|
||||
# t = 20
|
||||
# b = 20
|
||||
if isinstance(event, QKeyEvent):
|
||||
if event.key() == Qt.Key.Key_Left:
|
||||
self.index = max(0, self.index - 1)
|
||||
if self.index2page[self.index] in self.spreads:
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.label2.setText('not a spread')
|
||||
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
|
||||
self.label.setPixmap(pixmap)
|
||||
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# self.label2.setPixmap(pixmap2)
|
||||
elif event.key() == Qt.Key.Key_Right:
|
||||
self.index = min(self.index + 1, len(self.images) - 1)
|
||||
if self.index2page[self.index] in self.spreads:
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.label2.setText('not a spread')
|
||||
|
||||
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
|
||||
self.label.setPixmap(pixmap)
|
||||
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
|
||||
# self.label2.setPixmap(pixmap2)
|
||||
elif event.key() == Qt.Key.Key_Space:
|
||||
if self.label2.text() == "not a spread":
|
||||
self.spreads.append(os.path.basename(self.images[self.index]))
|
||||
self.label2.setText('spread')
|
||||
else:
|
||||
self.spreads.remove(os.path.basename(self.images[self.index]))
|
||||
self.label2.setText('not a spread')
|
||||
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
||||
self.accept()
|
||||
else:
|
||||
super().keyReleaseEvent(event)
|
||||
else:
|
||||
super().keyReleaseEvent(event)
|
||||
@@ -15,9 +15,9 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
|
||||
QFont, QFontDatabase, QGradient, QIcon,
|
||||
QImage, QKeySequence, QLinearGradient, QPainter,
|
||||
QPalette, QPixmap, QRadialGradient, QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QDialog, QGridLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QPushButton, QSizePolicy,
|
||||
QVBoxLayout, QWidget)
|
||||
from PySide6.QtWidgets import (QApplication, QCheckBox, QDialog, QGridLayout,
|
||||
QHBoxLayout, QLabel, QLineEdit, QPushButton,
|
||||
QSizePolicy, QVBoxLayout, QWidget)
|
||||
from . import KCC_rc
|
||||
|
||||
class Ui_editorDialog(object):
|
||||
@@ -57,6 +57,12 @@ class Ui_editorDialog(object):
|
||||
|
||||
self.gridLayout.addWidget(self.volumeLine, 1, 1, 1, 1)
|
||||
|
||||
self.bulkVolumeCheck = QCheckBox(self.editorWidget)
|
||||
self.bulkVolumeCheck.setObjectName(u"bulkVolumeCheck")
|
||||
self.bulkVolumeCheck.setVisible(False)
|
||||
|
||||
self.gridLayout.addWidget(self.bulkVolumeCheck, 1, 2, 1, 1)
|
||||
|
||||
self.label_3 = QLabel(self.editorWidget)
|
||||
self.label_3.setObjectName(u"label_3")
|
||||
|
||||
@@ -157,7 +163,8 @@ class Ui_editorDialog(object):
|
||||
self.verticalLayout.addWidget(self.optionWidget)
|
||||
|
||||
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.titleLine)
|
||||
QWidget.setTabOrder(self.volumeLine, self.bulkVolumeCheck)
|
||||
QWidget.setTabOrder(self.bulkVolumeCheck, self.titleLine)
|
||||
QWidget.setTabOrder(self.titleLine, self.numberLine)
|
||||
QWidget.setTabOrder(self.numberLine, self.writerLine)
|
||||
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
|
||||
@@ -175,6 +182,10 @@ class Ui_editorDialog(object):
|
||||
editorDialog.setWindowTitle(QCoreApplication.translate("editorDialog", u"Metadata editor", None))
|
||||
self.label_1.setText(QCoreApplication.translate("editorDialog", u"Series:", None))
|
||||
self.label_2.setText(QCoreApplication.translate("editorDialog", u"Volume:", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setToolTip(QCoreApplication.translate("editorDialog", u"<b>Bulk Volume Editing</b><br>Check this box to assign volume numbers to multiple files.<br><br><b>Input formats:</b><br><code>5</code> \u2192 sequence starting from 5 (5, 6, 7...)<br><code>1-10</code> \u2192 range from 1 to 10<br><code>1, 3, 5</code> \u2192 specific values<br><br><i>Note: Files are sorted alphabetically before assignment.</i>", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.bulkVolumeCheck.setText("")
|
||||
self.label_3.setText(QCoreApplication.translate("editorDialog", u"Number:", None))
|
||||
self.label_4.setText(QCoreApplication.translate("editorDialog", u"Writer:", None))
|
||||
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = '10.1.2'
|
||||
__version__ = '11.1.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
#
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from argparse import ArgumentParser
|
||||
from time import perf_counter, strftime, gmtime
|
||||
from copy import copy
|
||||
@@ -30,14 +34,14 @@ from glob import glob, escape
|
||||
from re import sub
|
||||
from stat import S_IWRITE, S_IREAD, S_IEXEC
|
||||
from typing import List
|
||||
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
|
||||
from tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||
from shutil import move, copytree, rmtree, copyfile
|
||||
from zipfile import ZipFile, ZIP_STORED
|
||||
from tempfile import mkdtemp, gettempdir
|
||||
from shutil import move, copytree, rmtree
|
||||
from multiprocessing import Pool, cpu_count
|
||||
from uuid import uuid4
|
||||
from natsort import os_sort_keygen, os_sorted
|
||||
from slugify import slugify as slugify_ext
|
||||
from PIL import Image, ImageFile
|
||||
from PIL import Image, ImageFile, ImageOps
|
||||
from pathlib import Path
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from psutil import virtual_memory, disk_usage
|
||||
@@ -74,22 +78,19 @@ def main(argv=None):
|
||||
print('No matching files found.')
|
||||
return 1
|
||||
if options.filefusion:
|
||||
fusion_source_parent = str(Path(sources[0]).parent)
|
||||
fusion_path = makeFusion(list(sources))
|
||||
sources.clear()
|
||||
sources.append(fusion_path)
|
||||
for source in sources:
|
||||
source = source.rstrip('\\').rstrip('/')
|
||||
options = copy(args)
|
||||
if options.filefusion and options.output is None:
|
||||
options.output = fusion_source_parent
|
||||
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
|
||||
|
||||
|
||||
@@ -144,7 +145,7 @@ def buildHTML(path, imgfile, imgfilepath, imgfile2=None):
|
||||
f.write('<div style="display:none;">.</div>\n')
|
||||
f.write(f'<img width="{imgsize[0]}" height="{imgsize[1]}" src="{"../" * backref}Images/{postfix}{imgfile}"/>\n')
|
||||
if imgfile2:
|
||||
f.write(f'<img style="bottom: 0" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
||||
f.write(f'<img style="top: 1920px" width="{imgsize2[0]}" height="{imgsize2[1]}" src="{"../" * backref}Images/{postfix}{imgfile2}"/>\n')
|
||||
f.write("</div>\n")
|
||||
if options.iskindle and options.panelview:
|
||||
if options.autoscale:
|
||||
@@ -227,7 +228,7 @@ def buildNCX(dstdir, title, chapters, chapternames):
|
||||
ncxfile = os.path.join(dstdir, 'OEBPS', 'toc.ncx')
|
||||
f = open(ncxfile, "w", encoding='UTF-8')
|
||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
"<ncx version=\"2005-1\" xml:lang=\"en-US\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
|
||||
f"<ncx version=\"2005-1\" xml:lang=\"{options.language}\" xmlns=\"http://www.daisy.org/z3986/2005/ncx/\">\n",
|
||||
"<head>\n",
|
||||
"<meta name=\"dtb:uid\" content=\"urn:uuid:", options.uuid, "\"/>\n",
|
||||
"<meta name=\"dtb:depth\" content=\"1\"/>\n",
|
||||
@@ -293,10 +294,22 @@ def buildNAV(dstdir, title, chapters, chapternames):
|
||||
def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
opffile = os.path.join(dstdir, 'OEBPS', 'content.opf')
|
||||
deviceres = options.profileData[1]
|
||||
if options.righttoleft:
|
||||
writingmode = "horizontal-rl"
|
||||
|
||||
if options.vertical4panel:
|
||||
writingmode = "vertical"
|
||||
else:
|
||||
writingmode = "horizontal-lr"
|
||||
writingmode = "horizontal"
|
||||
if options.invertdirection:
|
||||
if options.righttoleft:
|
||||
writingmode += "-lr"
|
||||
else:
|
||||
writingmode += "-rl"
|
||||
else:
|
||||
if options.righttoleft:
|
||||
writingmode += "-rl"
|
||||
else:
|
||||
writingmode += "-lr"
|
||||
|
||||
f = open(opffile, "w", encoding='UTF-8')
|
||||
f.writelines(["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n",
|
||||
"<package version=\"3.0\" unique-identifier=\"BookID\" ",
|
||||
@@ -304,7 +317,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
"<metadata xmlns:opf=\"http://www.idpf.org/2007/opf\" ",
|
||||
"xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n",
|
||||
"<dc:title>", hescape(title), "</dc:title>\n",
|
||||
"<dc:language>en-US</dc:language>\n",
|
||||
f"<dc:language>{options.language}</dc:language>\n",
|
||||
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
|
||||
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
|
||||
if len(options.summary) > 0:
|
||||
@@ -395,13 +408,22 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
else:
|
||||
return ""
|
||||
|
||||
if options.righttoleft:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||
pageside = "right"
|
||||
if options.invertdirection:
|
||||
if options.righttoleft:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||
pageside = "left"
|
||||
else:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||
pageside = "right"
|
||||
else:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||
pageside = "left"
|
||||
if originalpath.lower().endswith('.pdf'):
|
||||
if options.righttoleft:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"rtl\" toc=\"ncx\">\n")
|
||||
pageside = "right"
|
||||
else:
|
||||
f.write("</manifest>\n<spine page-progression-direction=\"ltr\" toc=\"ncx\">\n")
|
||||
pageside = "left"
|
||||
|
||||
if originalpath.lower().endswith('.pdf') or originalpath.lower().endswith('.epub'):
|
||||
if pageside == "right":
|
||||
pageside = "left"
|
||||
else:
|
||||
@@ -466,6 +488,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
pageside = "right"
|
||||
|
||||
for entry, prop in zip(reflist, page_spread_property_list):
|
||||
if options.onepagelandscape:
|
||||
prop = 'center'
|
||||
f.write(f'<itemref idref="page_{entry}" {pageSpreadProperty(prop)}/>\n')
|
||||
|
||||
f.write("</spine>\n</package>\n")
|
||||
@@ -849,7 +873,7 @@ def extract_page(vector):
|
||||
|
||||
|
||||
|
||||
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
|
||||
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height, pdfwidth):
|
||||
render = False
|
||||
with pymupdf.open(filename) as doc:
|
||||
for page in doc:
|
||||
@@ -868,8 +892,15 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
||||
|
||||
cpu = cpu_count()
|
||||
|
||||
if not render:
|
||||
print('PDF input can also use legacy extract option if you have any problems')
|
||||
if GUI:
|
||||
GUI.addMessage.emit('PDF input can also use legacy extract option if you have any problems'
|
||||
, 'info', False)
|
||||
GUI.addMessage.emit('', '', False)
|
||||
|
||||
# make vectors of arguments for the processes
|
||||
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
|
||||
vectors = [(i, cpu, filename, output_dir, target_width, target_height, pdfwidth) for i in range(cpu)]
|
||||
print("Starting %i processes for '%s'." % (cpu, filename))
|
||||
|
||||
|
||||
@@ -883,7 +914,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
||||
|
||||
|
||||
|
||||
def getWorkFolder(afile, workdir=None):
|
||||
def getWorkFolder(afile, options, workdir=None):
|
||||
if not workdir:
|
||||
if options.tempdir:
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
@@ -892,12 +923,17 @@ def getWorkFolder(afile, workdir=None):
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
else:
|
||||
fullPath = workdir
|
||||
check_path = gettempdir()
|
||||
|
||||
if options.tempdir:
|
||||
check_path = os.path.dirname(afile)
|
||||
else:
|
||||
check_path = gettempdir()
|
||||
|
||||
DISK_WARNING = "Not enough disk space to perform conversion. Try Temp Directory option."
|
||||
|
||||
if os.path.isdir(afile):
|
||||
if disk_usage(check_path)[2] < getDirectorySize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
raise UserWarning(DISK_WARNING)
|
||||
try:
|
||||
copytree(afile, fullPath)
|
||||
sanitizePermissions(fullPath)
|
||||
@@ -907,13 +943,13 @@ def getWorkFolder(afile, workdir=None):
|
||||
raise UserWarning("Failed to prepare a workspace.")
|
||||
elif os.path.isfile(afile):
|
||||
if disk_usage(check_path)[2]< os.path.getsize(afile) * 2.5:
|
||||
raise UserWarning("Not enough disk space to perform conversion.")
|
||||
raise UserWarning(DISK_WARNING)
|
||||
if afile.lower().endswith('.pdf'):
|
||||
if not os.path.exists(fullPath):
|
||||
os.makedirs(fullPath)
|
||||
path = workdir
|
||||
sanitizePermissions(path)
|
||||
if options.pdfextract:
|
||||
if options.legacyextract:
|
||||
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
|
||||
njpg = pdf.extract()
|
||||
if njpg == 0:
|
||||
@@ -927,7 +963,7 @@ def getWorkFolder(afile, workdir=None):
|
||||
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
|
||||
target_width *= 1.25
|
||||
try:
|
||||
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
|
||||
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height, options.pdfwidth)
|
||||
except Exception as e:
|
||||
rmtree(path, True)
|
||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
||||
@@ -938,6 +974,8 @@ def getWorkFolder(afile, workdir=None):
|
||||
try:
|
||||
cbx = comicarchive.ComicArchive(afile)
|
||||
path = cbx.extract(fullPath)
|
||||
if options.lightnovel:
|
||||
return workdir
|
||||
sanitizePermissions(path)
|
||||
|
||||
tdir = os.listdir(fullPath)
|
||||
@@ -949,19 +987,86 @@ def getWorkFolder(afile, workdir=None):
|
||||
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
|
||||
)
|
||||
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
|
||||
if options.tempdir:
|
||||
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
else:
|
||||
workdir2 = mkdtemp('', 'KCC-')
|
||||
fullPath2 = os.path.join(workdir2, 'OEBPS', 'Images')
|
||||
os.makedirs(fullPath2, exist_ok=True)
|
||||
for file in os.listdir(os.path.join(fullPath, tdir[0])):
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath)
|
||||
os.rmdir(os.path.join(fullPath, tdir[0]))
|
||||
move(os.path.join(fullPath, tdir[0], file), fullPath2)
|
||||
rmtree(workdir, True)
|
||||
return workdir2
|
||||
|
||||
if options.legacyextract:
|
||||
return workdir
|
||||
|
||||
if afile.lower().endswith('.epub'):
|
||||
container = ET.parse(os.path.join(path, 'META-INF', 'container.xml'))
|
||||
opf_path = container.find(r'.//{*}rootfile').attrib['full-path']
|
||||
opf_path = os.path.join(path, opf_path)
|
||||
opf = ET.parse(opf_path)
|
||||
spine = []
|
||||
for spine_item in opf.findall(r'.//{*}itemref'):
|
||||
spine.append(spine_item.attrib.get('idref'))
|
||||
manifest_dict = {}
|
||||
for manifest_item in opf.findall(".//*[@media-type='application/xhtml+xml']"):
|
||||
manifest_dict[manifest_item.attrib.get('id')] = manifest_item.attrib.get('href')
|
||||
ordered_image_paths = []
|
||||
for i, spine_item in enumerate(spine):
|
||||
try:
|
||||
page_path = os.path.join(os.path.dirname(opf_path), manifest_dict[spine_item])
|
||||
page = ET.parse(page_path)
|
||||
except Exception:
|
||||
continue
|
||||
imgs = page.findall(r'.//{*}img') + page.findall(r'.//{*}image')
|
||||
|
||||
largest_size = 0
|
||||
img_path = None
|
||||
for img in imgs:
|
||||
for key in img.attrib:
|
||||
if 'src' in key or 'href' in key:
|
||||
temp_img_path = img.attrib[key]
|
||||
if temp_img_path.startswith('..'):
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
else:
|
||||
temp_img_path = os.path.join(os.path.dirname(opf_path), os.path.dirname(manifest_dict[spine_item]), temp_img_path)
|
||||
try:
|
||||
temp_size = os.path.getsize(temp_img_path)
|
||||
if temp_size > largest_size:
|
||||
largest_size = temp_size
|
||||
img_path = temp_img_path
|
||||
except OSError:
|
||||
pass
|
||||
# TODO empty image
|
||||
if img_path:
|
||||
ordered_image_paths.append(img_path)
|
||||
# fallback if naive spine extraction fails
|
||||
if not ordered_image_paths:
|
||||
return workdir
|
||||
|
||||
if options.tempdir:
|
||||
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
else:
|
||||
workdir2 = mkdtemp('', 'KCC-')
|
||||
for i, img_path in enumerate(ordered_image_paths):
|
||||
_, ext = os.path.splitext(img_path)
|
||||
fullpath2 = os.path.join(workdir2, 'OEBPS', 'Images')
|
||||
os.makedirs(fullpath2, exist_ok=True)
|
||||
shutil.copyfile(img_path, os.path.join(fullpath2, f"{i}{ext}"))
|
||||
rmtree(workdir, True)
|
||||
return workdir2
|
||||
|
||||
return workdir
|
||||
|
||||
except OSError as e:
|
||||
rmtree(workdir, True)
|
||||
raise UserWarning(e)
|
||||
finally:
|
||||
pass
|
||||
else:
|
||||
raise UserWarning("Failed to open source file/directory.")
|
||||
|
||||
|
||||
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
if options.folder_output:
|
||||
ext = ''
|
||||
source_path = Path(srcpath)
|
||||
if srcpath[-1] == os.path.sep:
|
||||
srcpath = srcpath[:-1]
|
||||
@@ -973,7 +1078,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
ext = '.kepub.epub'
|
||||
if wantedname is not None:
|
||||
wanted_root, wanted_ext = os.path.splitext(wantedname)
|
||||
if wantedname.endswith(ext):
|
||||
if not options.folder_output and wantedname.endswith(ext):
|
||||
filename = os.path.abspath(wantedname)
|
||||
elif wanted_ext == '.mobi' and ext == '.epub':
|
||||
filename = os.path.abspath(wanted_root + ext)
|
||||
@@ -997,10 +1102,10 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
filename = source_path.with_name(name)
|
||||
else:
|
||||
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
|
||||
if os.path.isfile(filename):
|
||||
if os.path.exists(filename):
|
||||
counter = 0
|
||||
basename = os.path.splitext(filename)[0]
|
||||
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
|
||||
while os.path.exists(basename + '_kcc' + str(counter) + ext):
|
||||
counter += 1
|
||||
filename = basename + '_kcc' + str(counter) + ext
|
||||
elif options.format == 'MOBI' and ext == '.epub':
|
||||
@@ -1016,6 +1121,7 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
|
||||
def getMetadata(path, originalpath):
|
||||
xmlPath = os.path.join(path, 'ComicInfo.xml')
|
||||
options.comicinfo_chapters = []
|
||||
options.comicinfo_xml = None
|
||||
options.summary = ''
|
||||
titleSuffix = ''
|
||||
options.volume = ''
|
||||
@@ -1072,6 +1178,10 @@ def getMetadata(path, originalpath):
|
||||
options.summary = xml.data['Summary']
|
||||
if xml.data['Series']:
|
||||
options.series = xml.data['Series']
|
||||
# ComicInfo.xml in output may break readers like the Kobo native CBZ reader
|
||||
if options.keepcomicinfo and options.format == 'CBZ':
|
||||
with open(xmlPath, 'rb') as f:
|
||||
options.comicinfo_xml = f.read()
|
||||
os.remove(xmlPath)
|
||||
|
||||
if originalpath.lower().endswith('.pdf'):
|
||||
@@ -1114,14 +1224,22 @@ def removeNonImages(filetree):
|
||||
os.remove(os.path.join(root, name))
|
||||
# remove empty nested folders
|
||||
for root, dirs, files in os.walk(filetree, False):
|
||||
if not files and not dirs:
|
||||
if not os.listdir(root):
|
||||
os.rmdir(root)
|
||||
|
||||
if not os.listdir(Path(filetree).parent):
|
||||
raise UserWarning('No images detected, nested archives are not supported.')
|
||||
warning = [
|
||||
'No images detected.',
|
||||
'',
|
||||
'Possible causes:',
|
||||
'',
|
||||
'1) Incompatible image file extension like .jxl. Convert to .png first outside of KCC.',
|
||||
'2) Nested archive: Either extract the nested archive outside of KCC or use File Fusion option.',
|
||||
]
|
||||
raise RuntimeError('\n'.join(warning))
|
||||
|
||||
|
||||
def sanitizeTree(filetree, prefix='kcc'):
|
||||
def sanitizeTree(filetree, options, prefix='kcc'):
|
||||
chapterNames = {}
|
||||
page = 1
|
||||
cover_path = None
|
||||
@@ -1146,7 +1264,7 @@ def sanitizeTree(filetree, prefix='kcc'):
|
||||
dirs.sort(key=OS_SORT_KEY)
|
||||
for i, name in enumerate(dirs):
|
||||
tmpName = name
|
||||
slugified = slugify(name, is_natural_sorted)
|
||||
slugified = slugify(name, options, is_natural_sorted)
|
||||
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
|
||||
slugified += "A"
|
||||
chapterNames[slugified] = tmpName
|
||||
@@ -1167,6 +1285,7 @@ def flattenTree(filetree):
|
||||
|
||||
|
||||
def sanitizePermissions(filetree):
|
||||
os.chmod(filetree, S_IWRITE | S_IREAD | S_IEXEC)
|
||||
for root, dirs, files in os.walk(filetree, False):
|
||||
for name in files:
|
||||
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
|
||||
@@ -1308,7 +1427,7 @@ def createNewTome(parent):
|
||||
return tomePath, tomePathRoot
|
||||
|
||||
|
||||
def slugify(value, is_natural_sorted):
|
||||
def slugify(value, options, is_natural_sorted):
|
||||
if options.format == 'CBZ' and is_natural_sorted:
|
||||
return value
|
||||
if options.format != 'CBZ':
|
||||
@@ -1320,8 +1439,10 @@ def slugify(value, is_natural_sorted):
|
||||
return value
|
||||
|
||||
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||
if options.folder_output:
|
||||
move(basedir, zipfilename)
|
||||
return zipfilename
|
||||
start = perf_counter()
|
||||
zipfilename = os.path.abspath(zipfilename) + '.zip'
|
||||
if SEVENZIP in available_archive_tools():
|
||||
if isepub:
|
||||
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||
@@ -1364,10 +1485,20 @@ def makeParser():
|
||||
" [Default=KV]")
|
||||
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||
help="Manga style (right-to-left reading and splitting)")
|
||||
main_options.add_argument("--lightnovel", action="store_true", dest="lightnovel", default=False,
|
||||
help="Only resize images and preserve original file structure.")
|
||||
main_options.add_argument("--wallpaper", action="store_true", dest="wallpaper", default=False,
|
||||
help="Crop to fill screen.")
|
||||
main_options.add_argument("--ebok", action="store_true", dest="ebok", default=False,
|
||||
help="Force EBOK tag instead of PDOC for MOBI")
|
||||
main_options.add_argument("--invertdirection", action="store_true", dest="invertdirection", default=False,
|
||||
help="Invert page turn direction")
|
||||
main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
|
||||
help="Try to increase the quality of magnification")
|
||||
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
|
||||
help="Display two not four panels in Panel View mode")
|
||||
main_options.add_argument("--vertical4panel", action="store_true", dest="vertical4panel", default=False,
|
||||
help="Display side panels first in virtual panel view")
|
||||
main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
|
||||
help="Webtoon processing mode"),
|
||||
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
|
||||
@@ -1381,8 +1512,12 @@ def makeParser():
|
||||
output_options.add_argument("--metadatatitle", type=int, dest="metadatatitle", default=0,
|
||||
help="Write title using ComicInfo.xml or other embedded metadata. 1: Combine Title with default schema "
|
||||
"2: Use Title only")
|
||||
output_options.add_argument("--keepcomicinfo", type=int, dest="keepcomicinfo", default=0,
|
||||
help="Keep any original ComicInfo.xml files")
|
||||
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
|
||||
help="Author name [Default=KCC]")
|
||||
output_options.add_argument("--language", action="store", dest="language", default="en-US",
|
||||
help="EPUB language [Default=en-US]")
|
||||
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
|
||||
"[Default=Auto]")
|
||||
@@ -1393,6 +1528,8 @@ def makeParser():
|
||||
"2: Consider every subdirectory as separate volume [Default=0]")
|
||||
output_options.add_argument("--spreadshift", action="store_true", dest="spreadshift", default=False,
|
||||
help="Shift first page to opposite side in landscape for spread alignment")
|
||||
output_options.add_argument("--onepagelandscape", action="store_true", dest="onepagelandscape", default=False,
|
||||
help="Show a single centered page in landscape")
|
||||
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,
|
||||
@@ -1402,12 +1539,12 @@ def makeParser():
|
||||
|
||||
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("--legacyextract", action="store_true", dest="legacyextract", default=False,
|
||||
help="Use the legacy PDF/EPUB image extraction method from older KCC versions")
|
||||
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("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False,
|
||||
help="Disable attempt to crop main cover from wide image")
|
||||
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,
|
||||
@@ -1488,6 +1625,9 @@ def checkOptions(options):
|
||||
else:
|
||||
options.isKobo = True
|
||||
|
||||
if options.lightnovel:
|
||||
options.noKepub = True
|
||||
|
||||
if not options.iskindle and ('MOBI' in options.format or 'EPUB-200MB' in options.format or 'KFX' in options.format):
|
||||
raise UserWarning('MOBI/Send to Kindle not supported for non-Kindle profiles')
|
||||
|
||||
@@ -1513,6 +1653,12 @@ def checkOptions(options):
|
||||
options.keep_epub = True
|
||||
options.format = 'MOBI'
|
||||
options.kfx = False
|
||||
options.skip_zip = False
|
||||
options.folder_output = False
|
||||
if options.format == 'FOLDER':
|
||||
options.format = 'CBZ'
|
||||
options.skip_zip = True
|
||||
options.folder_output = True
|
||||
if options.format == 'Auto':
|
||||
if options.profile in ['KDX']:
|
||||
options.format = 'CBZ'
|
||||
@@ -1607,35 +1753,37 @@ def checkTools(source):
|
||||
except (FileNotFoundError, CalledProcessError):
|
||||
print('ERROR: KindleGen is missing!')
|
||||
sys.exit(1)
|
||||
except OSError as e:
|
||||
print(f"kindlegen: {e.strerror}")
|
||||
print('Re-install Rosetta/Kindle Previewer/other Intel app?')
|
||||
print('Please email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def checkPre(source):
|
||||
def checkPre(source='KCC-'):
|
||||
# Make sure that all temporary files are gone
|
||||
for root, dirs, _ in walkLevel(gettempdir(), 0):
|
||||
for tempdir in dirs:
|
||||
if tempdir.startswith('KCC-'):
|
||||
if tempdir.startswith(source):
|
||||
rmtree(os.path.join(root, tempdir), True)
|
||||
# Make sure that target directory is writable
|
||||
if os.path.isdir(source):
|
||||
src = os.path.abspath(os.path.join(source, '..'))
|
||||
else:
|
||||
src = os.path.dirname(source)
|
||||
try:
|
||||
with TemporaryFile(prefix='KCC-', dir=src):
|
||||
pass
|
||||
except Exception:
|
||||
raise UserWarning("Target directory is not writable.")
|
||||
|
||||
|
||||
def makeFusion(sources: List[str]):
|
||||
if len(sources) < 2:
|
||||
raise UserWarning('Fusion requires at least 2 sources. Did you forget to uncheck fusion?')
|
||||
start = perf_counter()
|
||||
first_path = Path(sources[0])
|
||||
if first_path.is_file():
|
||||
fusion_path = first_path.parent.joinpath(first_path.stem + ' [fused]')
|
||||
|
||||
if options.tempdir:
|
||||
fusion_parent = first_path.parent
|
||||
else:
|
||||
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
|
||||
# LLL is after KCC
|
||||
checkPre('LLL-')
|
||||
fusion_parent = Path(mkdtemp('', 'LLL-'))
|
||||
|
||||
if first_path.is_file():
|
||||
fusion_path = fusion_parent.joinpath(first_path.stem + ' [fused]')
|
||||
else:
|
||||
fusion_path = fusion_parent.joinpath(first_path.name + ' [fused]')
|
||||
print("Running Fusion")
|
||||
|
||||
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
|
||||
@@ -1644,7 +1792,6 @@ def makeFusion(sources: List[str]):
|
||||
|
||||
for index, source in enumerate(sources, start=1):
|
||||
print(f"Processing {source}...")
|
||||
checkPre(source)
|
||||
print("Checking images...")
|
||||
source_path = Path(source)
|
||||
# Add the fusion_0001_ prefix to maintain user-specified order if needed
|
||||
@@ -1656,8 +1803,10 @@ def makeFusion(sources: List[str]):
|
||||
else:
|
||||
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
|
||||
|
||||
getWorkFolder(source, str(targetpath))
|
||||
sanitizeTree(targetpath, prefix='fusion')
|
||||
path = getWorkFolder(source, options, str(targetpath))
|
||||
if path != str(targetpath):
|
||||
move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
|
||||
sanitizeTree(targetpath, options, prefix='fusion')
|
||||
# TODO: remove flattenTree when subchapters are supported
|
||||
flattenTree(targetpath)
|
||||
|
||||
@@ -1676,17 +1825,94 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
GUI.progressBarTick.emit('1')
|
||||
else:
|
||||
checkTools(source)
|
||||
checkPre(source)
|
||||
checkPre()
|
||||
if not options.filefusion:
|
||||
checkPre('LLL-')
|
||||
print(f"{job_progress}Preparing source images...")
|
||||
path = getWorkFolder(source)
|
||||
path = getWorkFolder(source, options)
|
||||
print(f"{job_progress}Checking images...")
|
||||
_, ext = os.path.splitext(source)
|
||||
|
||||
if options.lightnovel:
|
||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
for file in files:
|
||||
_, ext = os.path.splitext(file)
|
||||
if ext.lower() in ('.jpg', '.jpeg', '.png', '.webp', '.gif'):
|
||||
with Image.open(os.path.join(root, file)) as img:
|
||||
# TODO: detect BW images saved as RGB
|
||||
if not options.forcecolor:
|
||||
if img.mode == 'RGB':
|
||||
img = img.convert('L')
|
||||
elif img.mode == 'RGBA':
|
||||
img = img.convert('LA')
|
||||
x, y = image.ProfileData.Profiles[options.profile][1]
|
||||
if options.iskindle:
|
||||
x = min(x, 1920)
|
||||
y = min(y, 1920)
|
||||
if img.size[0] > x or img.size[1] > y:
|
||||
img = ImageOps.contain(img, (x, y))
|
||||
img.save(os.path.join(root, file), quality=options.jpegquality)
|
||||
if ext != '.epub':
|
||||
ext = '.cbz'
|
||||
output_file = getOutputFilename(source, options.output, ext, '')
|
||||
makeZIP(output_file, os.path.join(path, 'OEBPS', 'Images'), job_progress)
|
||||
rmtree(path, True)
|
||||
|
||||
return [output_file]
|
||||
|
||||
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
|
||||
removeNonImages(os.path.join(path, "OEBPS", "Images"))
|
||||
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
|
||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'), options)
|
||||
|
||||
if os.path.exists(source+'.json'):
|
||||
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
chapterNames = {}
|
||||
cover_path = None
|
||||
with open(source+'.json') as f:
|
||||
data = json.load(f)
|
||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
sorted_files = os_sorted(files)
|
||||
for i in range(len(sorted_files)):
|
||||
if not cover_path:
|
||||
cover_path = os.path.join(root, sorted_files[i])
|
||||
if i in data['spreads']:
|
||||
im1 = Image.open(os.path.join(root, sorted_files[i]))
|
||||
im2 = Image.open(os.path.join(root, sorted_files[i+1]))
|
||||
if not options.righttoleft:
|
||||
im1, im2 = im2, im1
|
||||
dst = Image.new('RGB', (im1.width + im2.width, im1.height))
|
||||
dst.paste(im2, (0, 0))
|
||||
dst.paste(im1, (im1.width, 0))
|
||||
base, _ = os.path.splitext(os.path.basename(sorted_files[i]))
|
||||
dst.save(os.path.join(path, 'OEBPS', 'Images', f'{base}-merged.png'))
|
||||
os.remove(os.path.join(root, sorted_files[i]))
|
||||
os.remove(os.path.join(root, sorted_files[i+1]))
|
||||
if not os.path.exists(cover_path):
|
||||
cover_path = os.path.join(root, f'{base}-merged.png')
|
||||
|
||||
if options.filefusion:
|
||||
# Strip the fusion_0001_ sort prefix from makeFusion if present
|
||||
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
|
||||
|
||||
source_path = Path(source)
|
||||
options.customcover = False
|
||||
if source_path.parent.joinpath('Covers').is_dir():
|
||||
series = os_sorted(filter(lambda s: s.endswith(ext) and '_kcc' not in s, os.listdir(source_path.parent)))
|
||||
source_index = series.index(os.path.basename(source))
|
||||
covers = os.listdir(source_path.parent.joinpath('Covers'))
|
||||
filtered_covers = []
|
||||
for cover in covers:
|
||||
_, cover_ext = getImageFileName(cover)
|
||||
if cover_ext in IMAGE_TYPES:
|
||||
filtered_covers.append(cover)
|
||||
sorted_covers = os_sorted(filtered_covers)
|
||||
try:
|
||||
cover_path = source_path.parent.joinpath('Covers', sorted_covers[source_index])
|
||||
options.customcover = True
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
cover = None
|
||||
if not options.webtoon:
|
||||
cover = image.Cover(cover_path, options)
|
||||
@@ -1768,15 +1994,18 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
||||
else:
|
||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||
if cover and cover.smartcover:
|
||||
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
|
||||
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
|
||||
if cover and (cover.smartcover or options.customcover):
|
||||
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', '##cover.jpg'), tomeNumber, len(tomes))
|
||||
if options.comicinfo_xml:
|
||||
with open(os.path.join(tome, 'OEBPS', 'Images', 'ComicInfo.xml'), 'wb') as xmlOutput:
|
||||
xmlOutput.write(options.comicinfo_xml)
|
||||
makeZIP(filepath[-1], os.path.join(tome, "OEBPS", "Images"), job_progress)
|
||||
elif options.format == 'PDF':
|
||||
print(f"{job_progress}Creating PDF file with PyMuPDF...")
|
||||
# determine output filename based on source and tome count
|
||||
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
|
||||
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
|
||||
if cover and cover.smartcover:
|
||||
if cover and (cover.smartcover or options.customcover):
|
||||
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
|
||||
# use optimized buildPDF logic with streaming and compression
|
||||
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
|
||||
@@ -1789,19 +2018,11 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
else:
|
||||
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
|
||||
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
|
||||
makeZIP(tome + '_comic', tome, job_progress, True)
|
||||
# Copy files to final destination (PDF files are already saved directly)
|
||||
if options.format != 'PDF':
|
||||
copyfile(tome + '_comic.zip', filepath[-1])
|
||||
try:
|
||||
os.remove(tome + '_comic.zip')
|
||||
except FileNotFoundError:
|
||||
# newly temporary created file is not found. It might have been already deleted
|
||||
pass
|
||||
makeZIP(filepath[-1], tome, job_progress, True)
|
||||
rmtree(tome, True)
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('tick')
|
||||
if not GUI and options.format == 'MOBI':
|
||||
if not GUI and options.format == 'MOBI' and not options.lightnovel:
|
||||
print(f"{job_progress}Creating MOBI files...")
|
||||
work = []
|
||||
for i in filepath:
|
||||
@@ -1820,8 +2041,6 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
if not output[0]:
|
||||
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
|
||||
return filepath
|
||||
else:
|
||||
os.remove(i.replace('.epub', '.mobi') + '_toclean')
|
||||
if cover and k.path and k.coverSupport:
|
||||
options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1])
|
||||
if options.delete:
|
||||
@@ -1832,22 +2051,23 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
|
||||
end = perf_counter()
|
||||
print(f"{job_progress}makeBook: {end - start} seconds")
|
||||
# Clean up temporary workspace
|
||||
try:
|
||||
rmtree(path, True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if options.filefusion:
|
||||
rmtree(source, True)
|
||||
checkPre('LLL-')
|
||||
|
||||
return filepath
|
||||
|
||||
|
||||
def makeMOBIFix(item, uuid):
|
||||
is_pdoc = options.profile in image.ProfileData.ProfilesKindlePDOC.keys()
|
||||
if options.ebok:
|
||||
is_pdoc = False
|
||||
if not options.keep_epub:
|
||||
os.remove(item)
|
||||
mobiPath = item.replace('.epub', '.mobi')
|
||||
move(mobiPath, mobiPath + '_toclean')
|
||||
try:
|
||||
dualmetafix.DualMobiMetaFix(mobiPath + '_toclean', mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
|
||||
dualmetafix.DualMobiMetaFix(mobiPath, bytes(uuid, 'UTF-8'), is_pdoc)
|
||||
return [True]
|
||||
except Exception as err:
|
||||
return [False, format(err)]
|
||||
@@ -1857,8 +2077,14 @@ def makeMOBIWorkerTick(output):
|
||||
makeMOBIWorkerOutput.append(output)
|
||||
if output[0] != 0:
|
||||
makeMOBIWorkerPool.terminate()
|
||||
for warning in output[3]:
|
||||
print(warning)
|
||||
if GUI:
|
||||
GUI.progressBarTick.emit('tick')
|
||||
if output[3]:
|
||||
for warning in output[3]:
|
||||
GUI.addMessage.emit(warning, 'warning', False)
|
||||
GUI.addMessage.emit('', '', False)
|
||||
if not GUI.conversionAlive:
|
||||
makeMOBIWorkerPool.terminate()
|
||||
|
||||
@@ -1868,33 +2094,58 @@ def makeMOBIWorker(item):
|
||||
kindlegenErrorCode = 0
|
||||
kindlegenError = ''
|
||||
try:
|
||||
# TODO: This size check is incorrect, I think kindlegen increased the limit
|
||||
if os.path.getsize(item) < 629145600:
|
||||
start = perf_counter()
|
||||
# TODO: should anything be done with the kindlegen output during successes?
|
||||
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
|
||||
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
|
||||
end = perf_counter()
|
||||
print(f"kindlegen: {end - start} sec")
|
||||
else:
|
||||
# ERROR: EPUB too big
|
||||
kindlegenErrorCode = 23026
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
return [kindlegenErrorCode, kindlegenError, item, []]
|
||||
except CalledProcessError as err:
|
||||
warnings = []
|
||||
for line in err.stdout.splitlines():
|
||||
# ERROR: Generic error
|
||||
if "Error(" in line:
|
||||
kindlegenErrorCode = 1
|
||||
kindlegenError = line
|
||||
kindlegenError = '\n\n'.join(warnings + [line, 'kindlegen logs dumped'])
|
||||
try:
|
||||
timestamp = datetime.now().isoformat(timespec='milliseconds').replace(':', '-').replace('.', '-')
|
||||
with open(os.path.join(os.path.dirname(item), f'kindlegen-log-{timestamp}.txt'), 'w') as f:
|
||||
f.write(err.stdout)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# examples
|
||||
# Warning(prcgen):W14019: Cover is too small
|
||||
if "Warning(" in line:
|
||||
if ":W14016: Cover not specified" in line and options.webtoon:
|
||||
pass
|
||||
else:
|
||||
warnings.append(line)
|
||||
# ERROR: EPUB too big
|
||||
if ":E23026:" in line:
|
||||
kindlegenErrorCode = 23026
|
||||
if ":E23028:" in line:
|
||||
kindlegenErrorCode = 23028
|
||||
if kindlegenErrorCode > 0:
|
||||
break
|
||||
if ":I1036: Mobi file built successfully" in line:
|
||||
return [0, '', item]
|
||||
return [0, '', item, warnings]
|
||||
if ":I1037: Mobi file built with WARNINGS!" in line:
|
||||
return [0, '', item]
|
||||
return [0, '', item, warnings]
|
||||
# ERROR: KCC unknown generic error
|
||||
if kindlegenErrorCode == 0:
|
||||
kindlegenErrorCode = err.returncode
|
||||
kindlegenError = err.stdout
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
kindlegenErrorCode = -1
|
||||
if err.returncode == 3221226505:
|
||||
kindlegenError = f'Error {err.returncode}: Unknown Windows error. Possibly filepath too long?'
|
||||
else:
|
||||
kindlegenError = f'Error {err.returncode}'
|
||||
return [kindlegenErrorCode, kindlegenError, item, warnings]
|
||||
|
||||
|
||||
def makeMOBI(work, qtgui=None):
|
||||
|
||||
@@ -112,6 +112,8 @@ def splitImage(work):
|
||||
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
|
||||
|
||||
widthImg, heightImg = imgOrg.size
|
||||
if widthImg < 300:
|
||||
raise UserWarning('PDF render failed < 300 px. Try legacy extract option instead.')
|
||||
if heightImg > opt.height:
|
||||
if opt.debug:
|
||||
drawImg = Image.open(filePath).convert(mode='RGBA')
|
||||
|
||||
@@ -22,7 +22,6 @@ from functools import cached_property, lru_cache
|
||||
import os
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import distro
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from xml.dom.minidom import parseString
|
||||
from xml.parsers.expat import ExpatError
|
||||
@@ -41,12 +40,12 @@ class ComicArchive:
|
||||
self.dirname, self.basename = os.path.split(filepath)
|
||||
|
||||
@cached_property
|
||||
def type(self):
|
||||
def type(self):
|
||||
extraction_commands = [
|
||||
[SEVENZIP, 'l', '-y', '-p1', self.basename],
|
||||
]
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'l', '-y', '-p1', self.basename],
|
||||
)
|
||||
@@ -85,20 +84,20 @@ class ComicArchive:
|
||||
|
||||
extraction_commands.reverse()
|
||||
|
||||
if distro.id() == 'fedora' or distro.like() == 'fedora':
|
||||
if platform.system() == 'Linux':
|
||||
extraction_commands.append(
|
||||
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
|
||||
)
|
||||
|
||||
|
||||
for cmd in extraction_commands:
|
||||
try:
|
||||
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
|
||||
return targetdir
|
||||
return targetdir
|
||||
except FileNotFoundError:
|
||||
missing.append(cmd[0])
|
||||
except CalledProcessError:
|
||||
pass
|
||||
|
||||
|
||||
if missing:
|
||||
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
|
||||
else:
|
||||
|
||||
@@ -136,12 +136,11 @@ def del_exth(rec0, exth_num):
|
||||
|
||||
|
||||
class DualMobiMetaFix:
|
||||
def __init__(self, infile, outfile, asin, is_pdoc):
|
||||
def __init__(self, outfile, asin, is_pdoc):
|
||||
cdetype = b'EBOK'
|
||||
if is_pdoc:
|
||||
cdetype = b'PDOC'
|
||||
|
||||
shutil.copyfile(infile, outfile)
|
||||
f = open(outfile, "r+b")
|
||||
self.datain = mmap.mmap(f.fileno(), 0)
|
||||
self.datain_rec0 = readsection(self.datain, 0)
|
||||
|
||||
@@ -106,6 +106,7 @@ class ProfileData:
|
||||
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
|
||||
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
|
||||
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
|
||||
'KS1324': ("Kindle 1324", (1324, 1986), Palette16, 1.0),
|
||||
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
|
||||
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
|
||||
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
|
||||
@@ -164,7 +165,10 @@ class ComicPageParser:
|
||||
with Image.open(srcImgPath) as im:
|
||||
self.image = im.copy()
|
||||
|
||||
self.fill = self.fillCheck()
|
||||
self.page_background_color = self.fillCheck()
|
||||
self.fill = self.page_background_color
|
||||
if self.opt.bordersColor:
|
||||
self.fill = self.opt.bordersColor
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
@@ -194,9 +198,9 @@ class ComicPageParser:
|
||||
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
|
||||
new_image.paste(pageone, (0, 0))
|
||||
new_image.paste(pagetwo, (0, height))
|
||||
self.payload.append(['N', self.source, new_image, self.fill])
|
||||
self.payload.append(['N', self.source, new_image, self.page_background_color, self.fill])
|
||||
elif self.opt.webtoon:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
||||
# rotate only TODO dead code?
|
||||
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth and self.opt.splitter == 1:
|
||||
spread = self.image
|
||||
@@ -205,11 +209,12 @@ class ComicPageParser:
|
||||
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])
|
||||
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
|
||||
# elif wide enough to split
|
||||
elif (width > height) != (dstwidth > dstheight) and width / height > 1.16:
|
||||
# if (split) or (split and rotate)
|
||||
if self.opt.splitter != 1 and width / height < 1.75:
|
||||
BISECT_THRESHOLD = 1.8
|
||||
if self.opt.splitter != 1 and width / height < BISECT_THRESHOLD:
|
||||
if width > height:
|
||||
leftbox = (0, 0, int(width / 2), height)
|
||||
rightbox = (int(width / 2), 0, width, height)
|
||||
@@ -222,23 +227,23 @@ class ComicPageParser:
|
||||
else:
|
||||
pageone = self.image.crop(leftbox)
|
||||
pagetwo = self.image.crop(rightbox)
|
||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||
self.payload.append(['S1', self.source, pageone, self.page_background_color, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.page_background_color, self.fill])
|
||||
|
||||
# if (rotate) or (split and rotate)
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= BISECT_THRESHOLD):
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
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])
|
||||
self.payload.append(['R', self.source, spread, self.page_background_color, self.fill])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
self.payload.append(['N', self.source, self.image, self.page_background_color, self.fill])
|
||||
|
||||
def fillCheck(self):
|
||||
if self.opt.bordersColor:
|
||||
if False:
|
||||
return self.opt.bordersColor
|
||||
else:
|
||||
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
||||
@@ -277,7 +282,7 @@ class ComicPageParser:
|
||||
|
||||
|
||||
class ComicPage:
|
||||
def __init__(self, options, mode, path, image, fill):
|
||||
def __init__(self, options, mode, path, image, page_background_color, fill):
|
||||
self.opt = options
|
||||
_, self.size, self.palette, self.gamma = self.opt.profileData
|
||||
if self.opt.hq:
|
||||
@@ -287,6 +292,7 @@ class ComicPage:
|
||||
self.image = image.convert("RGB")
|
||||
self.color = self.colorCheck()
|
||||
self.colorOutput = self.color and self.opt.forcecolor
|
||||
self.page_background_color = page_background_color
|
||||
self.fill = fill
|
||||
self.rotated = False
|
||||
self.orgPath = os.path.join(path[0], path[1])
|
||||
@@ -507,7 +513,9 @@ class ComicPage:
|
||||
self.image = erase_rainbow_artifacts(self.image, is_color)
|
||||
|
||||
def resizeImage(self):
|
||||
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
|
||||
if self.opt.wallpaper:
|
||||
pass
|
||||
elif self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
|
||||
# TODO: Kindle Scribe case
|
||||
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
|
||||
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
|
||||
@@ -530,6 +538,8 @@ class ComicPage:
|
||||
self.image = ImageOps.pad(self.image, self.opt.kfx_resolution, method=method, color=self.fill)
|
||||
elif self.opt.stretch:
|
||||
self.image = self.image.resize(self.size, method)
|
||||
elif self.opt.wallpaper:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||
pass
|
||||
else: # if image bigger than device resolution or smaller with upscaling
|
||||
@@ -560,7 +570,7 @@ class ComicPage:
|
||||
self.image = self.image.crop(box)
|
||||
|
||||
def cropPageNumber(self, power, minimum):
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
||||
bbox = get_bbox_crop_margin_page_number(self.image, power, self.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -570,7 +580,7 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, 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.page_background_color)
|
||||
|
||||
if bbox:
|
||||
w, h = self.image.size
|
||||
@@ -580,7 +590,7 @@ class ComicPage:
|
||||
self.maybeCrop(bbox, minimum)
|
||||
|
||||
def cropInterPanelEmptySections(self, direction):
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
||||
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.page_background_color)
|
||||
|
||||
class Cover:
|
||||
def __init__(self, source, opt):
|
||||
@@ -598,7 +608,7 @@ class Cover:
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
if not self.options.forcecolor:
|
||||
self.image = self.image.convert('L')
|
||||
if not self.options.nosmartcovercrop:
|
||||
if self.options.smartcovercrop:
|
||||
self.crop_main_cover()
|
||||
|
||||
size = list(self.options.profileData[1])
|
||||
@@ -642,7 +652,7 @@ class Cover:
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((w * .36, 0, w, h))
|
||||
else:
|
||||
self.image = self.image.crop((w, 0, .64 * w, h))
|
||||
self.image = self.image.crop((0, 0, .64 * w, h))
|
||||
|
||||
def save_to_folder(self, target, tomeid, len_tomes=0):
|
||||
try:
|
||||
|
||||
@@ -122,10 +122,6 @@ def dependencyCheck(level):
|
||||
missing.append('PySide 6.0.0')
|
||||
except ImportError:
|
||||
missing.append('PySide 6.0.0+')
|
||||
try:
|
||||
import raven
|
||||
except ImportError:
|
||||
missing.append('raven 6.0.0+')
|
||||
if level > 1:
|
||||
try:
|
||||
from psutil import __version__ as psutilVersion
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
# Below requirements are compiled in Dockefile
|
||||
# numpy==2.3.4
|
||||
# PyMuPDF==1.26.6
|
||||
@@ -1,12 +1,10 @@
|
||||
PySide6==6.4.3
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy<2
|
||||
PyMuPDF>=1.26.1
|
||||
PyMuPDF==1.25.5
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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
|
||||
psutil>=7.2.2
|
||||
requests>=2.32.4
|
||||
python-slugify>=8.0.4
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy==1.23.0
|
||||
numpy==1.23.5
|
||||
PyMuPDF>=1.16
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
PySide6<6.10
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
python-slugify>=1.2.1,<9.0.0
|
||||
raven>=6.0.0
|
||||
packaging>=23.2
|
||||
psutil>=7.2.2
|
||||
requests>=2.34.2
|
||||
python-slugify>=8.0.4,<9.0.0
|
||||
packaging>=26.2
|
||||
mozjpeg-lossless-optimization>=1.2.0
|
||||
natsort>=8.4.0
|
||||
distro>=1.8.0
|
||||
numpy>=1.22.4
|
||||
PyMuPDF>=1.18.0
|
||||
|
||||
@@ -38,7 +38,7 @@ class BuildBinaryCommand(setuptools.Command):
|
||||
def run(self):
|
||||
VERSION = __version__
|
||||
if sys.platform == 'darwin':
|
||||
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
|
||||
os.system('pyinstaller -y kcc-macos.spec')
|
||||
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
|
||||
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
|
||||
if min_os.startswith('10.1'):
|
||||
@@ -153,11 +153,10 @@ setuptools.setup(
|
||||
'psutil>=5.9.5',
|
||||
'requests>=2.31.0',
|
||||
'python-slugify>=1.2.1,<9.0.0',
|
||||
'raven>=6.0.0',
|
||||
'mozjpeg-lossless-optimization>=1.2.0',
|
||||
'natsort>=8.4.0',
|
||||
'distro>=1.8.0',
|
||||
'numpy>=1.22.4',
|
||||
'packaging>=23.2',
|
||||
'PyMuPDF>=1.16.1',
|
||||
],
|
||||
classifiers=[],
|
||||
|
||||