1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-16 14:08:45 +00:00

Compare commits

...

51 Commits

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

* restore margins of top buttons

* fix button height on windows

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

* disable png extra optons by default

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

* add colorOutput variable

* fix typos

* remove dither

* add box

* clarify png

* remove debug code

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

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

* Update README to move cover cropping from FAQ to USAGE

* rename to coverfill

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

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

* don't sort items in GUI

* Add prefix only if sorted order is different

* Simplified sort prefix that will be removed from chapter name

* Restore adding prefix only when sorted order differs

* simplify prefix code

---------

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

* add default target size for Remarkable to 95

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

Removed image verification step before copying the image.

* Update image.py
2026-01-20 20:48:18 -08:00
Alex Xu
f5fd2bb7fe fix cropping divide by zero (#1220) 2026-01-20 20:47:56 -08:00
16 changed files with 1792 additions and 1252 deletions

View File

@@ -35,7 +35,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt
- name: build binary
run: |
@@ -68,7 +68,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
*.AppImage*

View File

@@ -38,7 +38,7 @@ jobs:
cache: 'pip'
- name: Install python dependencies
run: |
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
python -m pip install --upgrade pip pyinstaller certifi
pip install -r requirements.txt
- name: Install the Apple certificate and provisioning profile
# TODO signing
@@ -89,7 +89,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.dmg
- name: Clean up keychain and provisioning profile

View File

@@ -40,7 +40,7 @@ jobs:
- name: Install Python dependencies
run: |
python3 --version
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
pip3 install --upgrade pip pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- uses: actions/setup-node@v6
@@ -60,7 +60,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
dist/*.dmg

View File

@@ -45,7 +45,7 @@ jobs:
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
python -m pip install --upgrade pip setuptools wheel
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary
@@ -73,6 +73,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.exe

View File

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

View File

@@ -4,7 +4,7 @@ FROM python:3.13-slim-bullseye AS builder
# Install system dependencies
RUN set -x && \
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev python3-setuptools python3-wheel" && \
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev" && \
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}

View File

@@ -13,6 +13,13 @@ Pages display in fullscreen without margins,
with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux.
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
@@ -34,6 +41,7 @@ KCC avoids many common formatting issues (some of which occur [even on the Kindl
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
The GUI looks like this, built in Qt6, with my most commonly used settings:
@@ -46,7 +54,9 @@ You can change the default output directory by holding `Shift` while clicking th
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=IR2Fhcm9658
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
@@ -98,7 +108,7 @@ There are also legacy macOS 10.14+ and Windows 7 experimental versions available
The `c2e` and `c2p` versions are command line tools for power users.
On Mac, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
@@ -238,6 +248,8 @@ MAIN:
PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
--pdfwidth Render vector PDFs based on device width instead of height.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
-r SPLITTER, --splitter SPLITTER
@@ -258,8 +270,14 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
--smartcovercrop Attempt to crop main cover from wide image
--coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG
--forcepng Create PNG files instead JPEG for black and white images
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
--force-png-rgb Force color images to be saved as PNG
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
--noquantize Don't quantize PNG images to 16 colors
--mozjpeg Create JPEG files using mozJpeg
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
--maximizestrips Turn 1x4 strips to 2x2 strips
@@ -280,6 +298,7 @@ OUTPUT SETTINGS:
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option.
--rotateright Rotate double page spreads in opposite direction.
--rotatefirst Put rotated spread first in spread splitter option.
--filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies

1438
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -42,7 +42,7 @@ from raven import Client
from tempfile import gettempdir
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, available_archive_tools
from .comicarchive import SEVENZIP, TAR, available_archive_tools
from . import __version__
from . import comic2ebook
from . import metadata
@@ -195,7 +195,7 @@ class VersionThread(QThread):
icon = 'bindle'
if category == 'kofi':
icon = 'kofi'
message = f"<b>{payload.get('name')}</b>"
message = f"{payload.get('name')}"
if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'):
@@ -327,6 +327,14 @@ class WorkerThread(QThread):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.pdfExtractBox.isChecked():
options.pdfextract = True
if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True
if GUI.smartCoverCropBox.isChecked():
options.smartcovercrop = True
if GUI.coverFillBox.isChecked():
options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
@@ -341,12 +349,22 @@ class WorkerThread(QThread):
options.filefusion = False
if GUI.noRotateBox.isChecked():
options.norotate = True
if GUI.rotateRightBox.isChecked():
options.rotateright = True
if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked():
options.noquantize = True
if GUI.jpegQualityBox.isChecked():
options.jpegquality = GUI.jpegQualitySpinBox.value()
if GUI.currentMode > 2:
@@ -521,6 +539,7 @@ class WorkerThread(QThread):
if os.path.exists(item.replace('.epub', '.mobi')):
os.remove(item.replace('.epub', '.mobi'))
MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False)
MW.addMessage.emit(self.kindlegenErrorCode[1], 'error', False)
MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical')
if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '':
MW.showDialog.emit("KindleGen error:\n\n" + self.kindlegenErrorCode[1], 'error')
@@ -677,6 +696,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
def openHumble(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://humblebundleinc.sjv.io/3JaR3A'))
def openYouTube(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://www.youtube.com/@eink-dude'))
def openDiscord(self):
# noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://discord.gg/um5JRKwmGT'))
def modeChange(self, mode):
if mode == 1:
self.currentMode = 1
@@ -811,6 +842,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if bad_format in current_format:
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
break
GUI.pngLegacyBox.setEnabled(True)
GUI.noQuantizeBox.setEnabled(True)
GUI.forcePngRgbBox.setEnabled(True)
else:
GUI.pngLegacyBox.setEnabled(False)
GUI.noQuantizeBox.setEnabled(False)
GUI.forcePngRgbBox.setEnabled(False)
def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value)
@@ -878,6 +917,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if bad_format in current_format:
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
break
elif profile['Label'] == 'KDX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
if not profile['PVOptions']:
GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other':
@@ -904,7 +947,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
elif not GUI.webtoonBox.isChecked():
GUI.chunkSizeCheckBox.setEnabled(True)
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
def stripTags(self, html):
s = HTMLStripper()
@@ -1032,9 +1075,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'smartCoverCropBox': GUI.smartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(),
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
'jpegQuality': GUI.jpegQualitySpinBox.value(),
'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState(),
@@ -1042,6 +1094,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
'noRotateBox': GUI.noRotateBox.checkState(),
'rotateRightBox': GUI.rotateRightBox.checkState(),
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
'maximizeStrips': GUI.maximizeStrips.checkState(),
'gammaSlider': float(self.gammaValue) * 100,
@@ -1086,7 +1139,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if message[-1] == '/':
message = message[:-1]
self.handleMessage(message)
GUI.jobList.sortItems()
# sorting may conflict with manual file fusion order
# GUI.jobList.sortItems()
def forceShutdown(self):
self.saveSettings(None)
@@ -1163,7 +1217,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'gridLayout_6', 'horizontalLayout_2']:
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
if self.windowSize == '0x0':
MW.resize(500, 500)
@@ -1173,6 +1227,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1195,6 +1250,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 1920x1920": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1920',
},
"Kindle 1240x1860": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
},
"Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
@@ -1304,6 +1362,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Kindle 1920x1920",
"Kindle 1860x1920",
"Kindle 1240x1860",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle Paperwhite 7/10",
@@ -1351,7 +1410,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'<a href="https://github.com/ciromattia/kcc/wiki/Important-tips">important tips</a>.',
'info')
self.tar = 'tar' in available_archive_tools()
self.tar = TAR in available_archive_tools()
self.sevenzip = SEVENZIP in available_archive_tools()
if not any([self.tar, self.sevenzip]):
self.addMessage('<a href="https://github.com/ciromattia/kcc#7-zip">Install 7z (link)</a>'
@@ -1366,6 +1425,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble)
GUI.youtubeButton.clicked.connect(self.openYouTube)
GUI.discordButton.clicked.connect(self.openDiscord)
GUI.convertButton.clicked.connect(self.convertStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
@@ -1433,6 +1495,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.croppingPowerSlider.setValue(int(self.options[option]))
self.changeCroppingPower(int(self.options[option]))
GUI.preserveMarginBox.setValue(self.options.get('preserveMarginBox', 0))
elif str(option) == "jpegQuality":
GUI.jpegQualitySpinBox.setValue(int(self.options[option]))
elif str(option) == "chunkSizeBox":
GUI.chunkSizeBox.setValue(int(self.options[option]))
else:

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -48,6 +48,7 @@ from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -352,6 +353,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
mt = 'image/png'
elif '.gif' == filename[1]:
mt = 'image/gif'
elif '.webp' == filename[1]:
mt = 'image/webp'
else:
mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
@@ -659,7 +662,7 @@ def imgDirectoryProcessing(path, job_progress=''):
raise UserWarning("Conversion interrupted.")
if len(workerOutput) > 0:
rmtree(os.path.join(path, '..', '..'), True)
raise RuntimeError("One of workers crashed. Cause: " + workerOutput[0][0], workerOutput[0][1])
raise RuntimeError("One of workers crashed. Maybe restart PC. Cause: " + workerOutput[0][0], workerOutput[0][1])
else:
rmtree(os.path.join(path, '..', '..'), True)
raise UserWarning("C2E: Source directory is empty.")
@@ -688,7 +691,6 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload:
img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon:
@@ -698,18 +700,24 @@ def imgFileProcessing(work):
img.gammaCorrectImage()
if not img.colorOutput:
img.convertToGrayscale()
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.eraserainbow, is_color)
img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
if is_color:
if img.colorOutput:
pass
elif opt.forcepng:
img.convertToGrayscale()
if opt.format != 'PDF':
if not opt.noquantize:
img.quantizeImage()
else:
img.convertToGrayscale()
if opt.format == 'PDF':
img.convertToGrayscale()
elif opt.profile == 'KDX' and opt.format == 'CBZ':
img.convertToGrayscale()
elif opt.pnglegacy:
img.convertToGrayscale()
output.append(img.saveToDir())
return output
except Exception:
@@ -739,7 +747,9 @@ def render_page(vector):
cpu = vector[1] # number of CPUs
filename = vector[2] # document filename
output_dir = vector[3]
target_height = vector[4]
target_width = vector[4]
target_height = vector[5]
pdf_width = vector[6]
with pymupdf.open(filename) as doc: # open the document
num_pages = doc.page_count # get number of pages
@@ -750,7 +760,10 @@ def render_page(vector):
for i in range(seg_from, seg_to): # work through our page segment
page = doc[i]
zoom = target_height / page.rect.height
if not pdf_width or page.rect.width > page.rect.height:
zoom = target_height / page.rect.height
else:
zoom = target_width / page.rect.width
mat = pymupdf.Matrix(zoom, zoom)
# TODO: decide colorspace earlier so later color check is cheaper.
# This is actually pretty hard when you have to deal with color vector text
@@ -800,9 +813,7 @@ def extract_page(vector):
if len(image_list) > 1:
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
if not image_list:
width, height = int(page.rect.width), int(page.rect.height)
blank_page = Image.new("RGB", (width, height), "white")
blank_page.save(output_path)
continue
else:
xref = image_list[0][0]
d = doc.extract_image(xref)
@@ -817,7 +828,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
render = False
with pymupdf.open(filename) as doc:
for page in doc:
@@ -837,7 +848,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
cpu = cpu_count()
# make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename))
@@ -854,6 +865,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def getWorkFolder(afile, workdir=None):
if not workdir:
workdir = mkdtemp('', 'KCC-')
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else:
fullPath = workdir
@@ -875,13 +887,21 @@ def getWorkFolder(afile, workdir=None):
os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
target_height = options.profileData[1][1]
if options.pdfextract:
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
njpg = pdf.extract()
if njpg == 0:
raise UserWarning("Failed to extract images from PDF file.")
return workdir
target_width, target_height = options.profileData[1]
if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
target_height *= 1.2 #Account for possible margin at the top and bottom
target_width *= 1.2
elif options.cropping == 2:
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
target_width *= 1.25
try:
mupdf_pdf_process_pages_parallel(afile, fullPath, target_height)
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -957,6 +977,13 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
while os.path.isfile(basename + '_kcc' + str(counter) + ext):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
elif options.format == 'MOBI' and ext == '.epub':
counter = 0
basename = os.path.splitext(filename)[0]
if os.path.isfile(basename + '.mobi'):
while os.path.isfile(basename + '_kcc' + str(counter) + '.mobi'):
counter += 1
filename = basename + '_kcc' + str(counter) + ext
return filename
@@ -1066,7 +1093,7 @@ def removeNonImages(filetree):
raise UserWarning('No images detected, nested archives are not supported.')
def sanitizeTree(filetree):
def sanitizeTree(filetree, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1076,7 +1103,7 @@ def sanitizeTree(filetree):
_, ext = getImageFileName(name)
# 9999 page limit
unique_name = f'kcc-{page:04}'
unique_name = f'{prefix}-{page:04}'
page += 1
newKey = os.path.join(root, unique_name + ext)
@@ -1269,12 +1296,15 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools():
if isepub:
mimetypeFile = open(os.path.join(basedir, 'mimetype'), 'w')
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
mimetypeFile.write('application/epub+zip')
mimetypeFile.close()
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
subprocess_run([SEVENZIP, 'a', '-mx0', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
# crazy hack to ensure mimetype is first when using 7zip
if isepub:
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir):
@@ -1337,11 +1367,21 @@ def makeParser():
help="Shift first page to opposite side in landscape for spread alignment")
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
help="Do not rotate double page spreads in spread splitter option.")
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
help="Rotate double page spreads in opposite direction.")
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
help="Put rotated 2 page spread first in spread splitter option.")
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
help="Render vector PDFs to device width instead of height.")
processing_options.add_argument("--smartcovercrop", action="store_true", dest="smartcovercrop", default=False,
help="Attempt to crop main cover from wide image")
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
@@ -1377,7 +1417,15 @@ def makeParser():
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
help="Create PNG files instead JPEG for black and white images")
processing_options.add_argument("--force-png-rgb", action="store_true", dest="force_png_rgb", default=False,
help="Force color images to be saved as PNG")
processing_options.add_argument("--webp", action="store_true", dest="webp", default=False,
help="Replace JPG with lossy WEBP and PNG with lossless WEBP")
processing_options.add_argument("--pnglegacy", action="store_true", dest="pnglegacy", default=False,
help="Use a more compatible 8 bit png instead of 4 bit")
processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
help="Don't quantize to 16 color PNG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
help="Create JPEG files using mozJpeg")
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
@@ -1404,6 +1452,11 @@ def checkOptions(options):
options.isKobo = False
options.bordersColor = None
options.keep_epub = False
if options.format == 'PDF-200MB':
options.targetsize = 195
options.format = 'PDF'
if options.batchsplit != 2:
options.batchsplit = 1
if options.format == 'EPUB-200MB':
options.targetsize = 195
options.format = 'EPUB'
@@ -1415,6 +1468,8 @@ def checkOptions(options):
options.format = 'MOBI'
if options.batchsplit != 2:
options.batchsplit = 1
if not options.targetsize and options.profile.startswith('Rmk'):
options.targetsize = 95
if options.format == 'MOBI+EPUB':
options.keep_epub = True
options.format = 'MOBI'
@@ -1460,9 +1515,6 @@ def checkOptions(options):
if 'Ko' in options.profile:
options.panelview = False
options.hq = False
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.customheight = 1200
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
if options.format == 'KFX':
options.format = 'EPUB'
@@ -1488,6 +1540,15 @@ def checkOptions(options):
options.jpegquality = 85
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
options.webp_output = options.format != 'PDF' and not options.kindle_azw3 and options.webp
# CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ':
options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1])
options.profileData[1][1] = 1200
if options.kindle_scribe_azw3:
options.profileData = list(image.ProfileData.Profiles[options.profile])
options.profileData[1] = list(options.profileData[1])
@@ -1539,17 +1600,26 @@ def makeFusion(sources: List[str]):
fusion_path = first_path.parent.joinpath(first_path.name + ' [fused]')
print("Running Fusion")
for source in sources:
# Check if prefix is needed when user-specified ordering differs from OS natural sorting
path_names = [Path(s).stem if Path(s).is_file() else Path(s).name for s in sources]
needs_prefix = os_sorted(path_names) != path_names
for index, source in enumerate(sources, start=1):
print(f"Processing {source}...")
checkPre(source)
print("Checking images...")
source_path = Path(source)
# Add the fusion_0001_ prefix to maintain user-specified order if needed
prefix = ''
if needs_prefix:
prefix = f'fusion_{index:04d}_'
if source_path.is_file():
targetpath = fusion_path.joinpath(source_path.stem)
targetpath = fusion_path.joinpath(f'{prefix}{source_path.stem}')
else:
targetpath = fusion_path.joinpath(source_path.name)
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
getWorkFolder(source, str(targetpath))
sanitizeTree(targetpath)
sanitizeTree(targetpath, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath)
@@ -1576,6 +1646,9 @@ def makeBook(source, qtgui=None, job_progress=''):
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
if options.filefusion:
# Strip the fusion_0001_ sort prefix from makeFusion if present
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}
cover = None
if not options.webtoon:
cover = image.Cover(cover_path, options)
@@ -1767,4 +1840,3 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput

View File

@@ -30,6 +30,7 @@ from .shared import IMAGE_TYPES, subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
class ComicArchive:
@@ -73,7 +74,7 @@ class ComicArchive:
missing = []
extraction_commands = [
['tar', '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
[TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
]
@@ -125,7 +126,7 @@ class ComicArchive:
def available_archive_tools():
available = []
for tool in ['tar', SEVENZIP, 'unar', 'unrar']:
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
available.append(tool)

View File

@@ -103,6 +103,7 @@ class ProfileData:
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
@@ -157,7 +158,7 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
# Image.open(srcImgPath).verify()
with Image.open(srcImgPath) as im:
self.image = im.copy()
@@ -196,7 +197,10 @@ class ComicPageParser:
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
@@ -217,7 +221,10 @@ class ComicPageParser:
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
if not self.opt.rotateright:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.fill])
@@ -270,6 +277,8 @@ class ComicPage:
self.original_color_mode = image.mode
# TODO: color check earlier
self.image = image.convert("RGB")
self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
@@ -288,8 +297,7 @@ class ComicPage:
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@cached_property
def color(self):
def colorCheck(self):
if self.original_color_mode in ("L", "1"):
return False
if self.opt.webtoon:
@@ -398,17 +406,23 @@ class ComicPage:
raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath):
if self.opt.forcepng:
if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
image.info.pop('transparency', None)
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
elif self.opt.kindle_azw3:
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else:
targetPath += '.jpg'
if self.opt.mozjpeg:
if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
elif self.opt.mozjpeg:
targetPath += '.jpg'
with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue()
@@ -416,6 +430,7 @@ class ComicPage:
with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
targetPath += '.jpg'
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath
@@ -500,7 +515,9 @@ class ComicPage:
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
else: # if image bigger than device resolution or smaller with upscaling
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
self.image = ImageOps.fit(self.image, self.size, method=method)
elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
@@ -508,7 +525,7 @@ class ComicPage:
self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self):
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
return Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
@@ -562,13 +579,18 @@ class Cover:
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.crop_main_cover()
if self.options.smartcovercrop:
self.crop_main_cover()
size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3:
size[0] = min(size[0], 1920)
size[1] = min(size[1], 1920)
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
if self.options.coverfill and not self.options.kindle_scribe_azw3:
# TODO: Kindle Scribe case
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
else:
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
def crop_main_cover(self):
w, h = self.image.size

View File

@@ -160,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
if not edge.height or not edge.width:
continue
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)

View File

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