1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-20 16:09:05 +00:00

Compare commits

...

28 Commits

Author SHA1 Message Date
Alex Xu
a7a9f35686 Update README.md 2026-04-19 10:27:52 -07:00
Alex Xu
d5146d02fc Update README.md 2026-04-19 10:10:18 -07:00
Alex Xu
b0374e127d Smarter covers (#1300)
* smart crop semi-wide covers

* make it even smarter

* make it even smarter

* adjust ratio
2026-04-18 07:39:23 -07:00
Alex Xu
894dbfc8a2 don't bisect images with < 1.16 or > 1.75 aspect ratios (#1301) 2026-04-17 18:03:31 -07:00
Alex Xu
72f98bb032 bump to 9.7.2 2026-04-16 13:30:08 -07:00
Alex Xu
96bf14d386 Kindle Scribe 2025 default is PDF (#1297) 2026-04-16 13:26:04 -07:00
Alex Xu
d4e1565e4a colorsoft is wrong too 2026-04-16 13:25:41 -07:00
Alex Xu
c3030e8bd1 Paperwhite 12th Gen is actually 1272x1696 2026-04-16 13:25:41 -07:00
Alex Xu
c5744117e3 bump to 9.7.1 2026-04-15 21:59:27 -07:00
Alex Xu
cd2eeb4d0f option: create temporary files directory on source file drive (#1296) 2026-04-15 21:40:03 -07:00
Alex Xu
e7b7054b0e smart cover crop is default on and implemented for all formats (#1295) 2026-04-15 21:25:08 -07:00
Alex Xu
6f26bd5874 add epub series metadata (#1294) 2026-04-15 17:22:00 -07:00
Alex Xu
d5ca8fb407 don't bisect images with aspect ratio > 2 (#1293) 2026-04-13 23:03:26 -07:00
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
8 changed files with 1647 additions and 1342 deletions

View File

@@ -50,6 +50,17 @@ jobs:
with: with:
name: windows7-build name: windows7-build
path: dist/*.exe 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 - name: Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')

View File

@@ -11,7 +11,7 @@
like Kindle, Kobo, ReMarkable, and more. like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, Pages display in fullscreen without margins,
with proper fixed layout support. with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs. 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. Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux. KCC runs on Windows, macOS, and Linux.
@@ -115,15 +115,14 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
## FAQ ## FAQ
- Should I use Calibre? - Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting. - No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
Additionally, it will break page numbers.
Viewing KCC output in Calibre will also not work properly. Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder. Direct USB dropping is reccomended.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- Blank pages? - Blank pages?
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. - May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast. You can try PDF output.
Going back a few pages and exiting and re-entering book should fix it temporarily. Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use? - What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable. - MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable or Kindle Scribe 2025.
- All options have additional information in tooltips if you hover over the option. - All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB - To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Kindle panel view not working? - Kindle panel view not working?
@@ -137,9 +136,6 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps. (no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- How to make AZW3 instead of MOBI? - How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons. - The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- Huge margins / slow page turns? - Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB. - You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
@@ -198,8 +194,9 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0), 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0), 'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0), 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0), 'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0), 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
@@ -270,14 +267,19 @@ PROCESSING:
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0] Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--nosmartcovercrop Disable attempt to crop main cover from wide image
--coverfill Center-crop only the cover to fill target device screen --coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale --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 --noquantize Don't quantize PNG images to 16 colors
--mozjpeg Create JPEG files using mozJpeg --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. --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 --maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not recoverable. -d, --delete Delete source file(s) or a directory. It's not recoverable.
--tempdir Create temporary files directory on source file drive.
OUTPUT SETTINGS: OUTPUT SETTINGS:
-o OUTPUT, --output OUTPUT -o OUTPUT, --output OUTPUT

1500
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -195,7 +195,7 @@ class VersionThread(QThread):
icon = 'bindle' icon = 'bindle'
if category == 'kofi': if category == 'kofi':
icon = 'kofi' icon = 'kofi'
message = f"<b>{payload.get('name')}</b>" message = f"{payload.get('name')}"
if payload.get('link'): if payload.get('link'):
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name')) message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
if payload.get('showDeadline'): if payload.get('showDeadline'):
@@ -331,6 +331,8 @@ class WorkerThread(QThread):
options.pdfextract = True options.pdfextract = True
if GUI.pdfWidthBox.isChecked(): if GUI.pdfWidthBox.isChecked():
options.pdfwidth = True options.pdfwidth = True
if GUI.noSmartCoverCropBox.isChecked():
options.nosmartcovercrop = True
if GUI.coverFillBox.isChecked(): if GUI.coverFillBox.isChecked():
options.coverfill = True options.coverfill = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
@@ -339,6 +341,8 @@ class WorkerThread(QThread):
options.metadatatitle = 2 options.metadatatitle = 2
if GUI.deleteBox.isChecked(): if GUI.deleteBox.isChecked():
options.delete = True options.delete = True
if GUI.tempDirBox.isChecked():
options.tempdir = True
if GUI.spreadShiftBox.isChecked(): if GUI.spreadShiftBox.isChecked():
options.spreadshift = True options.spreadshift = True
if GUI.fileFusionBox.isChecked(): if GUI.fileFusionBox.isChecked():
@@ -351,10 +355,16 @@ class WorkerThread(QThread):
options.rotateright = True options.rotateright = True
if GUI.rotateFirstBox.isChecked(): if GUI.rotateFirstBox.isChecked():
options.rotatefirst = True options.rotatefirst = True
if GUI.forcePngRgbBox.isChecked():
options.force_png_rgb = True
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked: if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
options.forcepng = True options.forcepng = True
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked: elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
options.mozjpeg = True options.mozjpeg = True
if GUI.webpBox.isChecked():
options.webp = True
if GUI.pngLegacyBox.isChecked():
options.pnglegacy = True
if GUI.noQuantizeBox.isChecked(): if GUI.noQuantizeBox.isChecked():
options.noquantize = True options.noquantize = True
if GUI.jpegQualityBox.isChecked(): if GUI.jpegQualityBox.isChecked():
@@ -688,6 +698,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
# noinspection PyCallByClass # noinspection PyCallByClass
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude')) 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): def modeChange(self, mode):
if mode == 1: if mode == 1:
self.currentMode = 1 self.currentMode = 1
@@ -822,6 +844,14 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if bad_format in current_format: 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') self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
break 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): def togglechunkSizeCheckBox(self, value):
GUI.chunkSizeWidget.setVisible(value) GUI.chunkSizeWidget.setVisible(value)
@@ -892,6 +922,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
elif profile['Label'] == 'KDX': elif profile['Label'] == 'KDX':
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked) GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked) GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
GUI.pngLegacyBox.setChecked(True)
if not profile['PVOptions']: if not profile['PVOptions']:
GUI.qualityBox.setChecked(False) GUI.qualityBox.setChecked(False)
if str(GUI.deviceBox.currentText()) == 'Other': if str(GUI.deviceBox.currentText()) == 'Other':
@@ -1048,15 +1079,20 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'disableProcessingBox': GUI.disableProcessingBox.checkState(), 'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(), 'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'pdfWidthBox': GUI.pdfWidthBox.checkState(), 'pdfWidthBox': GUI.pdfWidthBox.checkState(),
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(),
'coverFillBox': GUI.coverFillBox.checkState(), 'coverFillBox': GUI.coverFillBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(), 'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(), 'mozJpegBox': GUI.mozJpegBox.checkState(),
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
'webpBox': GUI.webpBox.checkState(),
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
'noQuantizeBox': GUI.noQuantizeBox.checkState(), 'noQuantizeBox': GUI.noQuantizeBox.checkState(),
'jpegQualityBox': GUI.jpegQualityBox.checkState(), 'jpegQualityBox': GUI.jpegQualityBox.checkState(),
'jpegQuality': GUI.jpegQualitySpinBox.value(), 'jpegQuality': GUI.jpegQualitySpinBox.value(),
'widthBox': GUI.widthBox.value(), 'widthBox': GUI.widthBox.value(),
'heightBox': GUI.heightBox.value(), 'heightBox': GUI.heightBox.value(),
'deleteBox': GUI.deleteBox.checkState(), 'deleteBox': GUI.deleteBox.checkState(),
'tempDirBox': GUI.tempDirBox.checkState(),
'spreadShiftBox': GUI.spreadShiftBox.checkState(), 'spreadShiftBox': GUI.spreadShiftBox.checkState(),
'fileFusionBox': GUI.fileFusionBox.checkState(), 'fileFusionBox': GUI.fileFusionBox.checkState(),
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(), 'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
@@ -1184,7 +1220,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'convertButton', 'formatBox']: 'convertButton', 'formatBox']:
getattr(GUI, element).setMinimumSize(QSize(0, 0)) getattr(GUI, element).setMinimumSize(QSize(0, 0))
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1) 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) getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
if self.windowSize == '0x0': if self.windowSize == '0x0':
MW.resize(500, 500) MW.resize(500, 500)
@@ -1224,10 +1260,10 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
}, },
"Kindle Scribe 3": { "Kindle Scribe 3": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS3',
}, },
"Kindle Scribe Colorsoft": { "Kindle Scribe Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': False, 'ForceColor': True, 'Label': 'KSCS',
}, },
"Kindle 11": { "Kindle 11": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'K11',
@@ -1236,7 +1272,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW5',
}, },
"Kindle Paperwhite 12": { "Kindle Paperwhite 12": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KO', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': False, 'Label': 'KPW6',
}, },
"Kindle Colorsoft": { "Kindle Colorsoft": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS', 'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'ForceColor': True, 'Label': 'KCS',
@@ -1392,6 +1428,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.editorButton.clicked.connect(self.selectFileMetaEditor) GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
GUI.wikiButton.clicked.connect(self.openWiki) GUI.wikiButton.clicked.connect(self.openWiki)
GUI.kofiButton.clicked.connect(self.openKofi) 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.convertButton.clicked.connect(self.convertStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma) GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox) GUI.gammaBox.stateChanged.connect(self.togglegammaBox)

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -310,6 +310,15 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"]) f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
for author in options.authors: for author in options.authors:
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"]) f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
if not options.iskindle and options.series:
f.writelines(['<meta property="belongs-to-collection" id="c02">', hescape(options.series), "</meta>\n"])
f.writelines(['<meta refines="#c02" property="collection-type">', "series", "</meta>\n"])
if options.volume and options.number:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(f"{options.volume}.{options.number}"), "</meta>\n"])
elif options.volume:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.volume), "</meta>\n"])
elif options.number:
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.number), "</meta>\n"])
f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n") f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n")
if cover: if cover:
f.write("<meta name=\"cover\" content=\"cover\"/>\n") f.write("<meta name=\"cover\" content=\"cover\"/>\n")
@@ -353,6 +362,8 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
mt = 'image/png' mt = 'image/png'
elif '.gif' == filename[1]: elif '.gif' == filename[1]:
mt = 'image/gif' mt = 'image/gif'
elif '.webp' == filename[1]:
mt = 'image/webp'
else: else:
mt = 'image/jpeg' mt = 'image/jpeg'
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" + f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
@@ -545,7 +556,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
f.close() f.close()
build_html_start = perf_counter() build_html_start = perf_counter()
if cover: if cover:
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes) cover.save_to_folder(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
dot_clean(path) dot_clean(path)
options.covers.append((cover, options.uuid)) options.covers.append((cover, options.uuid))
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')): for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
@@ -689,7 +700,6 @@ def imgFileProcessing(work):
workImg = image.ComicPageParser((dirpath, afile), opt) workImg = image.ComicPageParser((dirpath, afile), opt)
for i in workImg.payload: for i in workImg.payload:
img = image.ComicPage(opt, *i) img = image.ComicPage(opt, *i)
is_color = (opt.forcecolor and img.color)
if opt.cropping == 2 and not opt.webtoon: if opt.cropping == 2 and not opt.webtoon:
img.cropPageNumber(opt.croppingp, opt.croppingm) img.cropPageNumber(opt.croppingp, opt.croppingm)
if opt.cropping == 1 and not opt.webtoon: if opt.cropping == 1 and not opt.webtoon:
@@ -699,21 +709,23 @@ def imgFileProcessing(work):
img.gammaCorrectImage() img.gammaCorrectImage()
if not img.colorOutput:
img.convertToGrayscale()
img.autocontrastImage() img.autocontrastImage()
img.resizeImage() img.resizeImage()
img.optimizeForDisplay(opt.eraserainbow, is_color) img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
if is_color: if img.colorOutput:
pass pass
elif opt.forcepng: elif opt.forcepng:
img.convertToGrayscale()
if not opt.noquantize: if not opt.noquantize:
img.quantizeImage() img.quantizeImage()
if opt.format == 'PDF': if opt.format == 'PDF':
img.convertToGrayscale() img.convertToGrayscale()
elif opt.profile == 'KDX' and opt.format == 'CBZ': elif opt.profile == 'KDX' and opt.format == 'CBZ':
img.convertToGrayscale() img.convertToGrayscale()
else: elif opt.pnglegacy:
img.convertToGrayscale() img.convertToGrayscale()
output.append(img.saveToDir()) output.append(img.saveToDir())
return output return output
@@ -862,7 +874,8 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
def getWorkFolder(afile, workdir=None): def getWorkFolder(afile, workdir=None):
if not workdir: if not workdir:
workdir = mkdtemp('', 'KCC-') workdir = mkdtemp('', 'KCC-')
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile)) if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images') fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else: else:
fullPath = workdir fullPath = workdir
@@ -892,9 +905,11 @@ def getWorkFolder(afile, workdir=None):
return workdir return workdir
target_width, target_height = options.profileData[1] target_width, target_height = options.profileData[1]
if options.cropping == 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: 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: try:
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height) mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
except Exception as e: except Exception as e:
@@ -987,6 +1002,9 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = [] options.comicinfo_chapters = []
options.summary = '' options.summary = ''
titleSuffix = '' titleSuffix = ''
options.volume = ''
options.number = ''
options.series = ''
if options.title == 'defaulttitle': if options.title == 'defaulttitle':
defaultTitle = True defaultTitle = True
if os.path.isdir(originalpath): if os.path.isdir(originalpath):
@@ -1015,8 +1033,10 @@ def getMetadata(path, originalpath):
options.title = xml.data['Series'] options.title = xml.data['Series']
if xml.data['Volume']: if xml.data['Volume']:
titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2) titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
options.volume = xml.data['Volume']
if xml.data['Number']: if xml.data['Number']:
titleSuffix += ' #' + xml.data['Number'].zfill(3) titleSuffix += ' #' + xml.data['Number'].zfill(3)
options.number = xml.data['Number']
if options.metadatatitle == 1 and xml.data['Title']: if options.metadatatitle == 1 and xml.data['Title']:
titleSuffix += ': ' + xml.data['Title'] titleSuffix += ': ' + xml.data['Title']
options.title += titleSuffix options.title += titleSuffix
@@ -1034,6 +1054,8 @@ def getMetadata(path, originalpath):
options.comicinfo_chapters = xml.data['Bookmarks'] options.comicinfo_chapters = xml.data['Bookmarks']
if xml.data['Summary']: if xml.data['Summary']:
options.summary = xml.data['Summary'] options.summary = xml.data['Summary']
if xml.data['Series']:
options.series = xml.data['Series']
os.remove(xmlPath) os.remove(xmlPath)
if originalpath.lower().endswith('.pdf'): if originalpath.lower().endswith('.pdf'):
@@ -1294,12 +1316,12 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w') mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
mimetypeFile.write('application/epub+zip') mimetypeFile.write('application/epub+zip')
mimetypeFile.close() 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 # crazy hack to ensure mimetype is first when using 7zip
if isepub: if isepub:
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir) subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
else: else:
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED) zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
if isepub: if isepub:
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED) zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
for dirpath, _, filenames in os.walk(basedir): for dirpath, _, filenames in os.walk(basedir):
@@ -1373,6 +1395,8 @@ def makeParser():
help="Use the legacy PDF image extraction method from KCC 8 and earlier") 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, processing_options.add_argument("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
help="Render vector PDFs to device width instead of height.") 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("--coverfill", action="store_true", dest="coverfill", default=False, processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
help="Crop cover to fill screen") help="Crop cover to fill screen")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False, processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
@@ -1410,7 +1434,13 @@ def makeParser():
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False, output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies") help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False, 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, processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
help="Don't quantize to 16 color PNG") help="Don't quantize to 16 color PNG")
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False, processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
@@ -1421,6 +1451,8 @@ def makeParser():
help="Turn 1x4 strips to 2x2 strips") help="Turn 1x4 strips to 2x2 strips")
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False, processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
help="Delete source file(s) or a directory. It's not recoverable.") help="Delete source file(s) or a directory. It's not recoverable.")
processing_options.add_argument("--tempdir", action="store_true", dest="tempdir", default=False,
help="Create temporary files directory on source file drive.")
custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0, custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
help="Replace screen width provided by device profile") help="Replace screen width provided by device profile")
@@ -1528,6 +1560,8 @@ def checkOptions(options):
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format) 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.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 # CBZ files on Kindle DX/DXG support higher resolution
if options.profile == 'KDX' and options.format == 'CBZ': if options.profile == 'KDX' and options.format == 'CBZ':
options.profileData = list(image.ProfileData.Profiles[options.profile]) options.profileData = list(image.ProfileData.Profiles[options.profile])
@@ -1681,12 +1715,16 @@ def makeBook(source, qtgui=None, job_progress=''):
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber))) filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
if 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) makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
elif options.format == 'PDF': elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...") print(f"{job_progress}Creating PDF file with PyMuPDF...")
# determine output filename based on source and tome count # determine output filename based on source and tome count
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else '' suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
output_file = getOutputFilename(source, options.output, '.pdf', suffix) output_file = getOutputFilename(source, options.output, '.pdf', suffix)
if cover.smartcover:
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
# use optimized buildPDF logic with streaming and compression # use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file) output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
filepath.append(output_pdf) filepath.append(output_pdf)

View File

@@ -98,14 +98,15 @@ class ProfileData:
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0), 'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0), 'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0), 'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0), 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0), 'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0), 'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KPW6': ("Kindle Paperwhite 6", (1272, 1696), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0), 'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0), 'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0), 'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0), 'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0), 'KCS': ("Kindle Colorsoft", (1272, 1696), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0), 'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0), 'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
} }
@@ -193,8 +194,10 @@ class ComicPageParser:
new_image.paste(pageone, (0, 0)) new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height)) 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.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \ elif self.opt.webtoon:
and not self.opt.webtoon and self.opt.splitter == 1: self.payload.append(['N', self.source, self.image, 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 spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
if not self.opt.rotateright: if not self.opt.rotateright:
@@ -202,8 +205,10 @@ class ComicPageParser:
else: else:
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True) 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.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon: # elif wide enough to split
if self.opt.splitter != 1: 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:
if width > height: if width > height:
leftbox = (0, 0, int(width / 2), height) leftbox = (0, 0, int(width / 2), height)
rightbox = (int(width / 2), 0, width, height) rightbox = (int(width / 2), 0, width, height)
@@ -218,7 +223,9 @@ class ComicPageParser:
pagetwo = self.image.crop(rightbox) pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill]) self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill]) self.payload.append(['S2', self.source, pagetwo, self.fill])
if self.opt.splitter > 0:
# if (rotate) or (split and rotate)
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 1.75):
spread = self.image spread = self.image
if not self.opt.norotate: if not self.opt.norotate:
if not self.opt.rotateright: if not self.opt.rotateright:
@@ -277,6 +284,8 @@ class ComicPage:
self.original_color_mode = image.mode self.original_color_mode = image.mode
# TODO: color check earlier # TODO: color check earlier
self.image = image.convert("RGB") self.image = image.convert("RGB")
self.color = self.colorCheck()
self.colorOutput = self.color and self.opt.forcecolor
self.fill = fill self.fill = fill
self.rotated = False self.rotated = False
self.orgPath = os.path.join(path[0], path[1]) self.orgPath = os.path.join(path[0], path[1])
@@ -295,8 +304,7 @@ class ComicPage:
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@cached_property def colorCheck(self):
def color(self):
if self.original_color_mode in ("L", "1"): if self.original_color_mode in ("L", "1"):
return False return False
if self.opt.webtoon: if self.opt.webtoon:
@@ -405,17 +413,23 @@ class ComicPage:
raise RuntimeError('Cannot save image. ' + str(err)) raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath): 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) 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' targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False) image.save(targetPath, 'GIF', optimize=1, interlace=False)
else: else:
targetPath += '.png' targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1) image.save(targetPath, 'PNG', optimize=1)
else: else:
if self.opt.webp_output:
targetPath += '.webp'
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
elif self.opt.mozjpeg:
targetPath += '.jpg' targetPath += '.jpg'
if self.opt.mozjpeg:
with io.BytesIO() as output: with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality) image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue() input_jpeg_bytes = output.getvalue()
@@ -423,6 +437,7 @@ class ComicPage:
with open(targetPath, "wb") as output_jpeg_file: with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes) output_jpeg_file.write(output_jpeg_bytes)
else: else:
targetPath += '.jpg'
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality) image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath return targetPath
@@ -517,7 +532,7 @@ class ComicPage:
self.image = ImageOps.contain(self.image, self.size, method=method) self.image = ImageOps.contain(self.image, self.size, method=method)
def resize_method(self): 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 return Image.Resampling.BICUBIC
else: else:
return Image.Resampling.LANCZOS return Image.Resampling.LANCZOS
@@ -561,6 +576,7 @@ class Cover:
self.options = opt self.options = opt
self.source = source self.source = source
self.image = Image.open(source) self.image = Image.open(source)
self.smartcover = False
# backwards compatibility for Pillow >9.1.0 # backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'): if not hasattr(Image, 'Resampling'):
Image.Resampling = Image Image.Resampling = Image
@@ -571,6 +587,7 @@ class Cover:
self.image = ImageOps.autocontrast(self.image, preserve_tone=True) self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
if not self.options.forcecolor: if not self.options.forcecolor:
self.image = self.image.convert('L') self.image = self.image.convert('L')
if not self.options.nosmartcovercrop:
self.crop_main_cover() self.crop_main_cover()
size = list(self.options.profileData[1]) size = list(self.options.profileData[1])
@@ -586,17 +603,37 @@ class Cover:
def crop_main_cover(self): def crop_main_cover(self):
w, h = self.image.size w, h = self.image.size
if w / h > 2: if w / h > 2:
self.smartcover = True
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h)) self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h)) self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.83:
self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((w * .19, 0, w * .575, h))
else:
self.image = self.image.crop((w * .425, 0, .81 * w, h))
elif w / h > 1.7:
self.smartcover = True
if self.options.righttoleft:
self.image = self.image.crop((w * .2, 0, w * .583, h))
else:
self.image = self.image.crop((w * .417, 0, .8 * w, h))
elif w / h > 1.34: elif w / h > 1.34:
self.smartcover = True
if self.options.righttoleft: if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h)) self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else: else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h)) self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
elif w / h > 1.0:
self.smartcover = True
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))
def save_to_epub(self, target, tomeid, len_tomes=0): def save_to_folder(self, target, tomeid, len_tomes=0):
try: try:
if tomeid == 0: if tomeid == 0:
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality) self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)