Compare commits

...
13 Commits
Author SHA1 Message Date
Alex Xu fc331d4e19 bump to 11.0.2 2026-08-26 13:05:44 -07:00
Alex Xu adac558aab shift click label spreads for HQ preview (#1427) 2026-08-26 13:05:14 -07:00
Alex Xu c8cf50da0a fix spread label when it's the first page (#1426) 2026-08-26 11:40:25 -07:00
Alex Xu f38f1c45b2 update README.md 2026-08-26 09:25:07 -07:00
Alex Xu bd0328a12f bump to 11.0.1 2026-08-02 10:47:25 -07:00
Alex Xu 846a755761 fix label feature on Windows (#1413) 2026-08-01 15:58:45 -07:00
Alex Xu ff80d275e6 bump to 11.0.0 2026-07-31 22:26:11 -07:00
Alex Xu b3e1d393b6 Manually label spreads button (#1412)
* initial commit

* initial commit 2

* json it

* UI labels

* basics

* add todo

* options to getWorkFolder

* refactor options

* refactor UI

* cleanup

* pdf fixed size

* remove comment

* finalized json creation

* fix help text

* finalized

* remove print

* add youtube
2026-07-31 22:25:41 -07:00
Alex Xu b4c71b3b91 pass options to slugify (#1411) 2026-07-29 17:06:52 -07:00
Alex Xu 7e412f12ae pass options to getWorkFolder (#1410) 2026-07-29 16:41:34 -07:00
Nils Leo 2076775d94 Create the destination dir before flattening a single-subfolder archive (#1405) 2026-07-23 22:43:16 -07:00
Alex Xu 59df22b62d add sort order note to readme 2026-07-20 15:06:12 -07:00
Alex Xu ff5edc5ce5 fix: Destination path... already exists (#1404) 2026-07-20 14:21:42 -07:00
15 changed files with 563 additions and 291 deletions
+62 -32
View File
@@ -1,4 +1,8 @@
<img src="header.jpg" alt="Header Image" width="400"> Most manga conversion methods for ereaders add margins. But KCC enables true fullscreen from top to bottom.
![header](images/header_margins.jpeg)
(feat. Ya Boy Kongming! and Fire Force from a [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1) PDF source.)
# KCC # KCC
@@ -11,53 +15,77 @@
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 like CBZ, or PDFs.
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.
Supported input formats include JPG/PNG image files in folders, archives like CBZ/EPUB, or PDF.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
The absolute highest quality source files are DRM-free PDFs from [Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical.
The PDFs are often x12000 resolution or better, while even the 10" Kindle Scribe is only x2480.
KCC's main goal is maximum image quality at significantly smaller file size. For example, KCC
1) can compress a 600 MB manga volume from Humble Bundle to 100 MB.
This is mostly accomplished by downscaling to the native resolution of your specific device.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
2) fixes black levels to avoid gray/faded blacks found in many Kindle Store manga
![black](images/black.jpeg)
3) fixes [the rainbow effect on Kaleido 3 color eink](https://www.youtube.com/watch?v=Dw2HTJCGMhw) without blur:
![rainbow](images/rainbow2.jpeg)
4) can [semi-automatically detect 2-page spreads](https://www.youtube.com/watch?v=kfIX67f7Aqk) from pre-split single pages:
![before](images/spreadbefore3.jpeg)
You can view combined spreads either rotated or not:
![full](images/full.jpeg)
You can view the split halves either before or after combined spreads:
![half](images/half.jpeg)
## Demo
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder! 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 https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently. Then all your usb loaded comic files will show up next to all your normal books in your ereader library!
**NEW**: PDF output is now supported for direct conversion to reMarkable devices! The final result looks incredible on my 10" Kindle Scribe: https://www.youtube.com/watch?v=2CIHW2N9Enc
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical, **WARNING**: Kindle Scribe 2025 MOBI support may have blank pages. Use PDF instead.
which can be directly converted by KCC.
Its main feature is various optional image processing steps to look good on eink screens, The GUI looks like this, built in Qt6.
which have different requirements than normal LCD screens. There are a lot of options, but most people only care about the ones checked below.
Combining that with downscaling to your specific device's screen resolution Hover over each option to see details in a tooltip.
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as: ![settings](images/settings.png)
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
2) unneccessary margins at the bottom of the screen
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
The GUI looks like this, built in Qt6, with my most commonly used settings: If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac or OpenMTP.
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
Simply drag and drop your files/folders into the KCC window,
adjust your settings (hover over each option to see details in a tooltip),
and hit convert to create ereader optimized files.
You can change the default output directory by holding `Shift` while clicking the convert button.
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658 Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
## Kindle Jailbreak for KOreader
Jailbreaking to use KOreader is optional. KCC works just fine with the native mobi manga viewer.
But if you jailbreak your Kindle, you'll unlock additional features like:
1) compatibility with image formats better suited for manga like 4-bit PNG and WEBP. This enables better quality at **half the file size** compared to JPG which must be 8-bit.
This is visually lossless since eink screens are 4-bit.
2) no dependency on kindlegen, which can mean **twice as fast conversions** since you can convert to CBZ directly.
3) You can dither by partially checking the JPG/PNG box and can crop in KCC instead of KOreader. This may increase page turn speed and battery.
### A word of warning ### 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. **KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers. Amazon's tool is for comic publishers and involves a lot of manual effort, while **KCC** is for comic/manga readers.
@@ -147,6 +175,8 @@ You'll need to install various tools to access important but optional features.
### KindleGen ### KindleGen
(This is not needed for Koreader users on jailbroken Kindle. Just set output to CBZ instead.)
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it. On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
+77 -61
View File
@@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>644</width> <width>649</width>
<height>706</height> <height>706</height>
</rect> </rect>
</property> </property>
@@ -711,7 +711,27 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="2"> <item row="0" column="0">
<widget class="QPushButton" name="editorButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to edit directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Metadata Editor</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/editor.png</normaloff>:/Other/icons/editor.png</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="humbleButton"> <widget class="QPushButton" name="humbleButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -728,7 +748,7 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="2">
<widget class="QPushButton" name="kofiButton"> <widget class="QPushButton" name="kofiButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@@ -751,8 +771,8 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="0"> <item row="0" column="1">
<widget class="QPushButton" name="editorButton"> <widget class="QPushButton" name="labelSpreadsButton">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>0</width> <width>0</width>
@@ -760,14 +780,10 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</size> </size>
</property> </property>
<property name="toolTip"> <property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Shift+Click to edit directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string> <string>Hold shift while clicking for a higher quality preview.</string>
</property> </property>
<property name="text"> <property name="text">
<string>Metadata Editor</string> <string>Label Spreads</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/editor.png</normaloff>:/Other/icons/editor.png</iconset>
</property> </property>
</widget> </widget>
</item> </item>
@@ -1074,6 +1090,56 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<item row="1" column="1">
<widget class="QComboBox" name="deviceBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="directoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add directory containing JPG, PNG or GIF files to queue.&lt;br/&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;CBR, CBZ and CB7 files inside will not be processed!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add input folder(s)</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
</property>
</widget>
</item>
<item row="1" column="3"> <item row="1" column="3">
<widget class="QPushButton" name="convertButton"> <widget class="QPushButton" name="convertButton">
<property name="minimumSize"> <property name="minimumSize">
@@ -1099,36 +1165,6 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="3">
<widget class="QPushButton" name="clearButton">
<property name="minimumSize">
<size>
<width>0</width>
<height>30</height>
</size>
</property>
<property name="text">
<string>Clear list</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/clear.png</normaloff>:/Other/icons/clear.png</iconset>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="deviceBox">
<property name="minimumSize">
<size>
<width>0</width>
<height>28</height>
</size>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Target device.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QPushButton" name="fileButton"> <widget class="QPushButton" name="fileButton">
<property name="minimumSize"> <property name="minimumSize">
@@ -1149,26 +1185,6 @@ Keeping this file may crash some readers like the Kobo native CBZ reader.</strin
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="4">
<widget class="QPushButton" name="directoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Add directory containing JPG, PNG or GIF files to queue.&lt;br/&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;CBR, CBZ and CB7 files inside will not be processed!&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Add input folder(s)</string>
</property>
<property name="icon">
<iconset resource="KCC.qrc">
<normaloff>:/Other/icons/folder_new.png</normaloff>:/Other/icons/folder_new.png</iconset>
</property>
</widget>
</item>
<item row="1" column="4"> <item row="1" column="4">
<widget class="QComboBox" name="formatBox"> <widget class="QComboBox" name="formatBox">
<property name="minimumSize"> <property name="minimumSize">
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 438 KiB

View File

Before

Width:  |  Height:  |  Size: 921 KiB

After

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

+125 -34
View File
@@ -19,10 +19,11 @@
from datetime import datetime, timezone from datetime import datetime, timezone
import itertools import itertools
import json
from pathlib import Path from pathlib import Path
from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings) from PySide6.QtCore import (QSize, QUrl, Qt, Signal, QIODeviceBase, QEvent, QThread, QSettings)
from PySide6.QtGui import (QColor, QIcon, QPixmap, QDesktopServices) from PySide6.QtGui import (QColor, QIcon, QImage, QKeyEvent, QPixmap, QDesktopServices)
from PySide6.QtWidgets import (QApplication, QLabel, QListWidgetItem, QMainWindow, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView) from PySide6.QtWidgets import (QApplication, QDialogButtonBox, QHBoxLayout, QLabel, QListWidgetItem, QMainWindow, QSizePolicy, QSystemTrayIcon, QFileDialog, QMessageBox, QDialog, QAbstractItemView, QListView, QTreeView, QWidget)
from PySide6.QtNetwork import (QLocalSocket, QLocalServer) from PySide6.QtNetwork import (QLocalSocket, QLocalServer)
import os import os
@@ -38,10 +39,15 @@ from xml.sax.saxutils import escape
from psutil import Process from psutil import Process
from copy import copy from copy import copy
from packaging.version import Version from packaging.version import Version
from tempfile import gettempdir from tempfile import gettempdir, mkdtemp
from PIL import Image
from PIL.Image import Dither
from .KCC_spread_label import LabelSpreadsDialog
from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run from .shared import HTMLStripper, sanitizeTrace, walkLevel, subprocess_run
from .comicarchive import SEVENZIP, TAR, available_archive_tools from .comicarchive import SEVENZIP, TAR, available_archive_tools
from .comic2ebook import OS_SORT_KEY, flattenTree, getWorkFolder, removeNonImages, sanitizeTree
from . import __version__ from . import __version__
from . import comic2ebook from . import comic2ebook
from . import metadata from . import metadata
@@ -234,39 +240,9 @@ class ProgressThread(QThread):
self.running = False self.running = False
class WorkerThread(QThread): def get_options():
def __init__(self):
QThread.__init__(self)
self.conversionAlive = False
self.errors = False
self.kindlegenErrorCode = [0]
self.workerOutput = []
self.progressBarTick = MW.progressBarTick
self.addMessage = MW.addMessage
def __del__(self):
self.wait()
def sync(self):
self.conversionAlive = GUI.conversionAlive
def clean(self):
GUI.progress.content = ''
GUI.progress.stop()
GUI.needClean = True
MW.hideProgressBar.emit()
MW.addMessage.emit('<b>Conversion interrupted.</b>', 'error', False)
MW.addTrayMessage.emit('Conversion interrupted.', 'Critical')
MW.modeConvert.emit(1)
# noinspection PyUnboundLocalVariable
def run(self):
MW.modeConvert.emit(0)
parser = comic2ebook.makeParser() parser = comic2ebook.makeParser()
options = parser.parse_args() options = parser.parse_args()
argv = ''
currentJobs = []
options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label'] options.profile = GUI.profiles[str(GUI.deviceBox.currentText())]['Label']
gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format'] gui_current_format = GUI.formats[str(GUI.formatBox.currentText())]['format']
@@ -394,6 +370,41 @@ class WorkerThread(QThread):
if GUI.chunkSizeCheckBox.isChecked(): if GUI.chunkSizeCheckBox.isChecked():
options.targetsize = int(GUI.chunkSizeBox.value()) options.targetsize = int(GUI.chunkSizeBox.value())
return options, gui_current_format
class WorkerThread(QThread):
def __init__(self):
QThread.__init__(self)
self.conversionAlive = False
self.errors = False
self.kindlegenErrorCode = [0]
self.workerOutput = []
self.progressBarTick = MW.progressBarTick
self.addMessage = MW.addMessage
def __del__(self):
self.wait()
def sync(self):
self.conversionAlive = GUI.conversionAlive
def clean(self):
GUI.progress.content = ''
GUI.progress.stop()
GUI.needClean = True
MW.hideProgressBar.emit()
MW.addMessage.emit('<b>Conversion interrupted.</b>', 'error', False)
MW.addTrayMessage.emit('Conversion interrupted.', 'Critical')
MW.modeConvert.emit(1)
# noinspection PyUnboundLocalVariable
def run(self):
MW.modeConvert.emit(0)
argv = ''
currentJobs = []
options, gui_current_format = get_options()
for i in range(GUI.jobList.count()): for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do # Make sure that we don't consider any system message as job to do
if GUI.jobList.item(i).icon().isNull(): if GUI.jobList.item(i).icon().isNull():
@@ -659,6 +670,84 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.jobList.addItem(dname) GUI.jobList.addItem(dname)
GUI.jobList.scrollToBottom() GUI.jobList.scrollToBottom()
def labelSpreadsStart(self):
low_quality_preview = True
if QApplication.keyboardModifiers() == Qt.ShiftModifier:
low_quality_preview = False
currentJobs = []
# TODO: make this a function since it's copy pasted
for i in range(GUI.jobList.count()):
# Make sure that we don't consider any system message as job to do
if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text()))
for job in currentJobs:
images = []
spreads = []
options, _ = get_options()
options.profileData = [(600, 800), (600,800)]
path = getWorkFolder(job, options)
removeNonImages(path)
sanitizeTree(path, options)
flattenTree(path)
if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(job))
else:
workdir = mkdtemp('', 'KCC-')
for root, _, files in os.walk(path):
files.sort(key=OS_SORT_KEY)
start_index = 0
if job.endswith('.pdf') or job.endswith('.epub'):
start_index = 1
if options.spreadshift:
start_index = 0 if start_index == 1 else 1
for i in range(start_index, len(files), 2):
if i == len(files) - 1:
continue
# TODO: with statements
# TODO: ignore 1% of top and bottom too?
im1 = Image.open(os.path.join(root, files[i]))
size1 = im1.size
preview_crop1 = im1.crop((0, 0, 0.2*size1[0], size1[1]))
if low_quality_preview:
preview_crop1 = preview_crop1.convert('1', dither=Dither.NONE)
calculation_crop1 = im1.crop((0.01*size1[0], 0, 0.04*size1[0], size1[1])).convert('1', dither=Dither.NONE)
#crop1 = crop11
im2 = Image.open(os.path.join(root, files[i+1]))
size2 = im2.size
preview_crop2 = im2.crop((0.8*size2[0], 0, size2[0], size2[1]))
if low_quality_preview:
preview_crop2 = preview_crop2.convert('1', dither=Dither.NONE)
calculation_crop2 = im2.crop((0.96*size2[0], 0, .99* size2[0], size2[1])).convert('1', dither=Dither.NONE)
#crop2 = crop22
# dst = Image.new('1', (im1.width + im2.width, im1.height))
# dst.paste(im2, (0, 0))
# dst.paste(im1, (im1.width, 0))
hist1 = calculation_crop1.histogram()
hist2 = calculation_crop2.histogram()
# TODO: small percentage instead of zero
if hist1[0] == 0 or hist1[-1] == 0 or hist2[0] == 0 or hist2[-1] == 0:
continue
preview_mode = '1' if low_quality_preview else 'RGB'
dst = Image.new(preview_mode, (preview_crop1.width + preview_crop2.width, preview_crop1.height))
dst.paste(preview_crop2, (0, 0))
dst.paste(preview_crop1, (preview_crop1.width, 0))
dst.save(os.path.join(workdir, f'label-{i:04}.png'))
images.append(os.path.join(workdir, f'label-{i:04}.png'))
if images:
dlg = LabelSpreadsDialog(APP.primaryScreen().availableGeometry().height(), images, spreads)
dlg.setWindowTitle(job)
if dlg.exec() == 1:
with open(job+'.json', "w") as fp:
# TODO: not very clean to grab index from filename
spreads = [int(filename[6:10]) for filename in spreads]
json.dump({'spreads': sorted(set(spreads))} , fp)
rmtree(path, True)
rmtree(workdir, True)
def selectFileMetaEditor(self, sname): def selectFileMetaEditor(self, sname):
files = [] files = []
@@ -1436,6 +1525,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc", 'README': "https://github.com/ciromattia/kcc?tab=readme-ov-file#kcc",
'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq", 'FAQ': "https://github.com/ciromattia/kcc/blob/master/README.md#faq",
'WIKI': "https://github.com/ciromattia/kcc/wiki", 'WIKI': "https://github.com/ciromattia/kcc/wiki",
'YOUTUBE': "https://www.youtube.com/@eink-dude",
'TUTORIAL': "https://youtu.be/QQ6zJcMF2Iw?si=80rfm6DU6OUJdFqa", 'TUTORIAL': "https://youtu.be/QQ6zJcMF2Iw?si=80rfm6DU6OUJdFqa",
'EMAIL': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions", 'EMAIL': "https://github.com/ciromattia/kcc?tab=readme-ov-file#commissions",
'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations", 'DONATE': "https://github.com/ciromattia/kcc/blob/master/README.md#issues--new-features--donations",
@@ -1472,6 +1562,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
GUI.kofiButton.clicked.connect(self.openKofi) GUI.kofiButton.clicked.connect(self.openKofi)
GUI.humbleButton.clicked.connect(self.openHumble) GUI.humbleButton.clicked.connect(self.openHumble)
GUI.convertButton.clicked.connect(self.convertStart) GUI.convertButton.clicked.connect(self.convertStart)
GUI.labelSpreadsButton.clicked.connect(self.labelSpreadsStart)
GUI.gammaSlider.valueChanged.connect(self.changeGamma) GUI.gammaSlider.valueChanged.connect(self.changeGamma)
GUI.gammaBox.stateChanged.connect(self.togglegammaBox) GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
GUI.croppingBox.stateChanged.connect(self.togglecroppingBox) GUI.croppingBox.stateChanged.connect(self.togglecroppingBox)
+92
View File
@@ -0,0 +1,92 @@
import os
from PySide6.QtCore import (Qt)
from PySide6.QtGui import (QKeyEvent, QPixmap)
from PySide6.QtWidgets import (QHBoxLayout, QLabel, QDialog)
class LabelSpreadsDialog(QDialog):
def __init__(self, available_height, images, spreads):
super().__init__()
self.index = 0
self.images = images
self.spreads = spreads
self.index2page = {i: os.path.basename(image) for i, image in enumerate(images)}
self.setWindowTitle("TODO: Filename goes here")
# self.setGeometry(APP.primaryScreen().availableGeometry())
# self.setMaximumSize(APP.primaryScreen().availableSize())
self.available_height = available_height
layout = QHBoxLayout()
self.setLayout(layout)
label = QLabel()
label2 = QLabel()
self.label = label
self.label2 = label2
layout.addWidget(label)
layout.addWidget(label2)
label2.setText("not a spread")
help_text = [
"Use arrows to change index.",
"Use space bar to confirm spreads.",
"Use spread shift option to offset by 1.",
"Use enter key to confirm all spreads."
"Close window to cancel."
]
buttonLabel = QLabel('\n'.join(help_text))
layout.addWidget(buttonLabel)
# print(label.size())
# print(label.maximumSize())
# l, t, r, b = layout.getContentsMargins()
#label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
pixmap = QPixmap(images[0]).scaledToHeight(self.available_height * 0.9)
label.setPixmap(pixmap)
#label.setScaledContents(True)
#label2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# pixmap2 = QPixmap(images[0]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# label2.setPixmap(pixmap2)
#label2.setScaledContents(True)
#self.resize(pixmap2.width(), pixmap2.height())
def keyReleaseEvent(self, event):
# t = 20
# b = 20
if isinstance(event, QKeyEvent):
if event.key() == Qt.Key.Key_Left:
self.index = max(0, self.index - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Right:
self.index = min(self.index + 1, len(self.images) - 1)
if self.index2page[self.index] in self.spreads:
self.label2.setText('spread')
else:
self.label2.setText('not a spread')
pixmap = QPixmap(self.images[self.index]).scaledToHeight(self.available_height * 0.9)
self.label.setPixmap(pixmap)
# pixmap2 = QPixmap(images[self.index]).scaledToHeight(self.frameGeometry().height() - t - b - t - b)
# self.label2.setPixmap(pixmap2)
elif event.key() == Qt.Key.Key_Space:
if self.label2.text() == "not a spread":
self.spreads.append(os.path.basename(self.images[self.index]))
self.label2.setText('spread')
else:
self.spreads.remove(os.path.basename(self.images[self.index]))
self.label2.setText('not a spread')
elif event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
self.accept()
else:
super().keyReleaseEvent(event)
else:
super().keyReleaseEvent(event)
+63 -53
View File
@@ -26,7 +26,7 @@ class Ui_mainWindow(object):
def setupUi(self, mainWindow): def setupUi(self, mainWindow):
if not mainWindow.objectName(): if not mainWindow.objectName():
mainWindow.setObjectName(u"mainWindow") mainWindow.setObjectName(u"mainWindow")
mainWindow.resize(644, 706) mainWindow.resize(649, 706)
icon = QIcon() icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
mainWindow.setWindowIcon(icon) mainWindow.setWindowIcon(icon)
@@ -360,33 +360,39 @@ class Ui_mainWindow(object):
self.gridLayout_6 = QGridLayout(self.toolWidget) self.gridLayout_6 = QGridLayout(self.toolWidget)
self.gridLayout_6.setObjectName(u"gridLayout_6") self.gridLayout_6.setObjectName(u"gridLayout_6")
self.gridLayout_6.setContentsMargins(0, 0, 0, 0) self.gridLayout_6.setContentsMargins(0, 0, 0, 0)
self.editorButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton")
self.editorButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon2)
self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1)
self.humbleButton = QPushButton(self.toolWidget) self.humbleButton = QPushButton(self.toolWidget)
self.humbleButton.setObjectName(u"humbleButton") self.humbleButton.setObjectName(u"humbleButton")
self.humbleButton.setMinimumSize(QSize(0, 30)) self.humbleButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon() icon3 = QIcon()
icon2.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon3.addFile(u":/Brand/icons/Humble_H-Red.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.humbleButton.setIcon(icon2) self.humbleButton.setIcon(icon3)
self.gridLayout_6.addWidget(self.humbleButton, 0, 2, 1, 1) self.gridLayout_6.addWidget(self.humbleButton, 0, 3, 1, 1)
self.kofiButton = QPushButton(self.toolWidget) self.kofiButton = QPushButton(self.toolWidget)
self.kofiButton.setObjectName(u"kofiButton") self.kofiButton.setObjectName(u"kofiButton")
self.kofiButton.setMinimumSize(QSize(0, 30)) self.kofiButton.setMinimumSize(QSize(0, 30))
icon3 = QIcon() icon4 = QIcon()
icon3.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon4.addFile(u":/Brand/icons/kofi_symbol.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.kofiButton.setIcon(icon3) self.kofiButton.setIcon(icon4)
self.kofiButton.setIconSize(QSize(19, 16)) self.kofiButton.setIconSize(QSize(19, 16))
self.gridLayout_6.addWidget(self.kofiButton, 0, 1, 1, 1) self.gridLayout_6.addWidget(self.kofiButton, 0, 2, 1, 1)
self.editorButton = QPushButton(self.toolWidget) self.labelSpreadsButton = QPushButton(self.toolWidget)
self.editorButton.setObjectName(u"editorButton") self.labelSpreadsButton.setObjectName(u"labelSpreadsButton")
self.editorButton.setMinimumSize(QSize(0, 30)) self.labelSpreadsButton.setMinimumSize(QSize(0, 30))
icon4 = QIcon()
icon4.addFile(u":/Other/icons/editor.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.editorButton.setIcon(icon4)
self.gridLayout_6.addWidget(self.editorButton, 0, 0, 1, 1) self.gridLayout_6.addWidget(self.labelSpreadsButton, 0, 1, 1, 1)
self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2) self.gridLayout.addWidget(self.toolWidget, 0, 0, 1, 2)
@@ -543,39 +549,20 @@ class Ui_mainWindow(object):
self.gridLayout_4 = QGridLayout(self.buttonWidget) self.gridLayout_4 = QGridLayout(self.buttonWidget)
self.gridLayout_4.setObjectName(u"gridLayout_4") self.gridLayout_4.setObjectName(u"gridLayout_4")
self.gridLayout_4.setContentsMargins(0, 0, 0, 0) self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30))
self.convertButton.setFont(font)
icon5 = QIcon()
icon5.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon5)
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
self.clearButton = QPushButton(self.buttonWidget)
self.clearButton.setObjectName(u"clearButton")
self.clearButton.setMinimumSize(QSize(0, 30))
icon6 = QIcon()
icon6.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.clearButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
self.deviceBox = QComboBox(self.buttonWidget) self.deviceBox = QComboBox(self.buttonWidget)
self.deviceBox.setObjectName(u"deviceBox") self.deviceBox.setObjectName(u"deviceBox")
self.deviceBox.setMinimumSize(QSize(0, 28)) self.deviceBox.setMinimumSize(QSize(0, 28))
self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1) self.gridLayout_4.addWidget(self.deviceBox, 1, 1, 1, 1)
self.fileButton = QPushButton(self.buttonWidget) self.clearButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton") self.clearButton.setObjectName(u"clearButton")
self.fileButton.setMinimumSize(QSize(0, 30)) self.clearButton.setMinimumSize(QSize(0, 30))
icon7 = QIcon() icon5 = QIcon()
icon7.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon5.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon7) self.clearButton.setIcon(icon5)
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1) self.gridLayout_4.addWidget(self.clearButton, 0, 3, 1, 1)
self.directoryButton = QPushButton(self.buttonWidget) self.directoryButton = QPushButton(self.buttonWidget)
self.directoryButton.setObjectName(u"directoryButton") self.directoryButton.setObjectName(u"directoryButton")
@@ -588,6 +575,25 @@ class Ui_mainWindow(object):
self.gridLayout_4.addWidget(self.directoryButton, 0, 4, 1, 1) self.gridLayout_4.addWidget(self.directoryButton, 0, 4, 1, 1)
self.convertButton = QPushButton(self.buttonWidget)
self.convertButton.setObjectName(u"convertButton")
self.convertButton.setMinimumSize(QSize(0, 30))
self.convertButton.setFont(font)
icon6 = QIcon()
icon6.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.convertButton.setIcon(icon6)
self.gridLayout_4.addWidget(self.convertButton, 1, 3, 1, 1)
self.fileButton = QPushButton(self.buttonWidget)
self.fileButton.setObjectName(u"fileButton")
self.fileButton.setMinimumSize(QSize(0, 30))
icon7 = QIcon()
icon7.addFile(u":/Other/icons/document_new.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.fileButton.setIcon(icon7)
self.gridLayout_4.addWidget(self.fileButton, 0, 1, 1, 1)
self.formatBox = QComboBox(self.buttonWidget) self.formatBox = QComboBox(self.buttonWidget)
self.formatBox.setObjectName(u"formatBox") self.formatBox.setObjectName(u"formatBox")
self.formatBox.setMinimumSize(QSize(0, 28)) self.formatBox.setMinimumSize(QSize(0, 28))
@@ -848,12 +854,16 @@ class Ui_mainWindow(object):
"Keeping this file may crash some readers like the Kobo native CBZ reader.", None)) "Keeping this file may crash some readers like the Kobo native CBZ reader.", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.keepComicInfoBox.setText(QCoreApplication.translate("mainWindow", u"Keep ComicInfo.xml", None)) self.keepComicInfoBox.setText(QCoreApplication.translate("mainWindow", u"Keep ComicInfo.xml", None))
self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None)) self.editorButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to edit directory.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None)) self.editorButton.setText(QCoreApplication.translate("mainWindow", u"Metadata Editor", None))
self.humbleButton.setText(QCoreApplication.translate("mainWindow", u"Humble Bundle Referral", None))
self.kofiButton.setText(QCoreApplication.translate("mainWindow", u"Support me on Ko-fi", None))
#if QT_CONFIG(tooltip)
self.labelSpreadsButton.setToolTip(QCoreApplication.translate("mainWindow", u"Hold shift while clicking for a higher quality preview.", None))
#endif // QT_CONFIG(tooltip)
self.labelSpreadsButton.setText(QCoreApplication.translate("mainWindow", u"Label Spreads", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None)) self.chunkSizeWidget.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p>Warning: chunk size greater than default may cause<br/>performance/battery issues, especially on older devices.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
@@ -878,22 +888,22 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None)) self.heightBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Resolution of the target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None)) self.deviceBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Target device.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
#if QT_CONFIG(tooltip) self.clearButton.setText(QCoreApplication.translate("mainWindow", u"Clear list", None))
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None)) self.directoryButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add directory containing JPG, PNG or GIF files to queue.<br/><span style=\" font-weight:600;\">CBR, CBZ and CB7 files inside will not be processed!</span></p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add input folder(s)", None)) self.directoryButton.setText(QCoreApplication.translate("mainWindow", u"Add input folder(s)", None))
#if QT_CONFIG(tooltip)
self.convertButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Shift+Click to select the output directory for this list.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.convertButton.setText(QCoreApplication.translate("mainWindow", u"Convert", None))
#if QT_CONFIG(tooltip)
self.fileButton.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Add CBR, CBZ, CB7 or PDF file to queue.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.fileButton.setText(QCoreApplication.translate("mainWindow", u"Add input file(s)", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None)) self.formatBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Output format.</p></body></html>", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = '10.4.0' __version__ = '11.0.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'
+46 -13
View File
@@ -20,6 +20,7 @@
from collections import Counter from collections import Counter
from datetime import datetime from datetime import datetime
import json
import os import os
import pathlib import pathlib
import re import re
@@ -872,7 +873,7 @@ def extract_page(vector):
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height): def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height, pdfwidth):
render = False render = False
with pymupdf.open(filename) as doc: with pymupdf.open(filename) as doc:
for page in doc: for page in doc:
@@ -892,7 +893,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
cpu = cpu_count() cpu = cpu_count()
# make vectors of arguments for the processes # make vectors of arguments for the processes
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)] vectors = [(i, cpu, filename, output_dir, target_width, target_height, pdfwidth) for i in range(cpu)]
print("Starting %i processes for '%s'." % (cpu, filename)) print("Starting %i processes for '%s'." % (cpu, filename))
@@ -906,7 +907,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
def getWorkFolder(afile, workdir=None): def getWorkFolder(afile, options, workdir=None):
if not workdir: if not workdir:
if options.tempdir: if options.tempdir:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile)) workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
@@ -955,7 +956,7 @@ def getWorkFolder(afile, workdir=None):
target_height *= 1.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 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, options.pdfwidth)
except Exception as e: except Exception as e:
rmtree(path, True) rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}") raise UserWarning(f"Failed to extract images from PDF file. {e}")
@@ -979,9 +980,16 @@ def getWorkFolder(afile, workdir=None):
os.path.join(fullPath, tdir[0], 'ComicInfo.xml') os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
) )
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])): if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
if options.tempdir:
workdir2 = mkdtemp('', 'KCC-', os.path.dirname(afile))
else:
workdir2 = mkdtemp('', 'KCC-')
fullPath2 = os.path.join(workdir2, 'OEBPS', 'Images')
os.makedirs(fullPath2, exist_ok=True)
for file in os.listdir(os.path.join(fullPath, tdir[0])): for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath) move(os.path.join(fullPath, tdir[0], file), fullPath2)
os.rmdir(os.path.join(fullPath, tdir[0])) rmtree(workdir, True)
return workdir2
if options.legacyextract: if options.legacyextract:
return workdir return workdir
@@ -1222,7 +1230,7 @@ def removeNonImages(filetree):
raise RuntimeError('\n'.join(warning)) raise RuntimeError('\n'.join(warning))
def sanitizeTree(filetree, prefix='kcc'): def sanitizeTree(filetree, options, prefix='kcc'):
chapterNames = {} chapterNames = {}
page = 1 page = 1
cover_path = None cover_path = None
@@ -1247,7 +1255,7 @@ def sanitizeTree(filetree, prefix='kcc'):
dirs.sort(key=OS_SORT_KEY) dirs.sort(key=OS_SORT_KEY)
for i, name in enumerate(dirs): for i, name in enumerate(dirs):
tmpName = name tmpName = name
slugified = slugify(name, is_natural_sorted) slugified = slugify(name, options, is_natural_sorted)
while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper(): while os.path.exists(os.path.join(root, slugified)) and name.upper() != slugified.upper():
slugified += "A" slugified += "A"
chapterNames[slugified] = tmpName chapterNames[slugified] = tmpName
@@ -1410,7 +1418,7 @@ def createNewTome(parent):
return tomePath, tomePathRoot return tomePath, tomePathRoot
def slugify(value, is_natural_sorted): def slugify(value, options, is_natural_sorted):
if options.format == 'CBZ' and is_natural_sorted: if options.format == 'CBZ' and is_natural_sorted:
return value return value
if options.format != 'CBZ': if options.format != 'CBZ':
@@ -1775,10 +1783,10 @@ def makeFusion(sources: List[str]):
else: else:
targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}') targetpath = fusion_path.joinpath(f'{prefix}{source_path.name}')
path = getWorkFolder(source, str(targetpath)) path = getWorkFolder(source, options, str(targetpath))
if path != str(targetpath): if path != str(targetpath):
move(os.path.join(path, 'OEBPS', 'Images'), targetpath) move(os.path.join(path, 'OEBPS', 'Images'), targetpath)
sanitizeTree(targetpath, prefix='fusion') sanitizeTree(targetpath, options, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported # TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath) flattenTree(targetpath)
@@ -1801,7 +1809,7 @@ def makeBook(source, qtgui=None, job_progress=''):
if not options.filefusion: if not options.filefusion:
checkPre('LLL-') checkPre('LLL-')
print(f"{job_progress}Preparing source images...") print(f"{job_progress}Preparing source images...")
path = getWorkFolder(source) path = getWorkFolder(source, options)
print(f"{job_progress}Checking images...") print(f"{job_progress}Checking images...")
if options.lightnovel: if options.lightnovel:
@@ -1835,7 +1843,32 @@ def makeBook(source, qtgui=None, job_progress=''):
getMetadata(os.path.join(path, "OEBPS", "Images"), source) getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images")) removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source) detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images')) chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'), options)
if os.path.exists(source+'.json'):
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
chapterNames = {}
cover_path = None
with open(source+'.json') as f:
data = json.load(f)
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
sorted_files = os_sorted(files)
for i in range(len(sorted_files)):
if not cover_path:
cover_path = os.path.join(root, sorted_files[i])
if i in data['spreads']:
im1 = Image.open(os.path.join(root, sorted_files[i]))
im2 = Image.open(os.path.join(root, sorted_files[i+1]))
dst = Image.new('RGB', (im1.width + im2.width, im1.height))
dst.paste(im2, (0, 0))
dst.paste(im1, (im1.width, 0))
base, _ = os.path.splitext(os.path.basename(sorted_files[i]))
dst.save(os.path.join(path, 'OEBPS', 'Images', f'{base}-merged.png'))
os.remove(os.path.join(root, sorted_files[i]))
os.remove(os.path.join(root, sorted_files[i+1]))
if not os.path.exists(cover_path):
cover_path = os.path.join(root, f'{base}-merged.png')
if options.filefusion: if options.filefusion:
# Strip the fusion_0001_ sort prefix from makeFusion if present # Strip the fusion_0001_ sort prefix from makeFusion if present
chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()} chapterNames = {k: sub(r'^fusion_\d{4}_', '', v) for k, v in chapterNames.items()}