Compare commits

...
8 Commits
Author SHA1 Message Date
Alex Xu 74042791f6 add kindle previewer 4 warning 2026-09-14 16:06:39 -07:00
Alex Xu 7245fdf2e1 support kindle previewer 4 (#1439) 2026-09-14 15:56:18 -07:00
Alex Xu fe85274105 implement custom covers for webtoon and fusion (#1438) 2026-09-14 13:06:53 -07:00
Alex Xu cb7592dcd0 fix cropping with edge imperfections (#1435)
* improve detection of edge imperfections for cropping

* increase imperfections ratio

* fil in edge too

* fine tune the constants
2026-09-12 17:35:05 -07:00
Alex Xu ea532c709b Update README.md 2026-09-08 13:19:05 -07:00
Alex Xu 1ce92bac83 Update README.md 2026-09-08 11:53:00 -07:00
Alex Xu 5b67d4019e Update README.md 2026-09-06 22:25:38 -07:00
Alex Xu 2423e2e44a Add files via upload 2026-09-06 22:22:59 -07:00
7 changed files with 78 additions and 29 deletions
+22 -10
View File
@@ -55,19 +55,23 @@ You can view the split halves either before or after combined spreads:
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 ![settings](images/easy.png)
There are a lot of options, but most people only care about the ones checked above.
Hover over each option to see details in a tooltip.
NOTE: some checkboxes can have a half-check marks.
Then all your usb loaded comic files will show up next to all your normal books in your ereader library! Then all your usb loaded comic files will show up next to all your normal books in your ereader library!
Here's a quick video demo of how the process works (using expert mode):
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
The final result looks incredible on my 10" Kindle Scribe: https://www.youtube.com/watch?v=2CIHW2N9Enc The final result looks incredible on my 10" Kindle Scribe: https://www.youtube.com/watch?v=2CIHW2N9Enc
**WARNING**: Kindle Scribe 2025 MOBI support may have blank pages. Use PDF instead. **WARNING**: Kindle Scribe 2025 MOBI support is limited, converted books may have blank pages. Use PDF instead.
The GUI looks like this, built in Qt6.
There are a lot of options, but most people only care about the ones checked below.
Hover over each option to see details in a tooltip.
![settings](images/settings.png)
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac or OpenMTP. If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac or OpenMTP.
@@ -77,7 +81,7 @@ Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
## Kindle Jailbreak for KOreader ## Kindle Jailbreak for KOreader
Jailbreaking to use KOreader is optional. KCC works just fine with the native mobi manga viewer. Jailbreaking to use KOreader is optional. MOBI manga converted by KCC can be opened in Kindles native reader just fine
But if you jailbreak your Kindle, you'll unlock additional features like: But if you jailbreak your Kindle, you'll unlock additional features like:
@@ -86,6 +90,10 @@ But if you jailbreak your Kindle, you'll unlock additional features like:
2) no dependency on kindlegen, which can mean **twice as fast conversions** since you can convert to CBZ directly. 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. 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.
Common KOreader manga mistakes: https://youtu.be/gaakIh9p8tw?si=X7OgPJlAvL9XGbLR
Set per folder setting defaults: https://youtu.be/O9p6HVWIQ6s?si=Jtx607erCBA_auWM
### 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.
@@ -175,7 +183,11 @@ You'll need to install various tools to access important but optional features.
### KindleGen ### KindleGen
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 and `kindlegen` will be autodetected from it.
> [!WARNING]
> On Sept 14, 2026, Kindle Previewer was updated to version 4, which made kindlegen harder to use.
> In the meantime, please download the older version 3: https://web.archive.org/web/20260819184257/https://kdp.amazon.com/en_US/help/topic/G202131170
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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 619 KiB

+1
View File
@@ -33,6 +33,7 @@ def modify_path():
mac_paths = [ mac_paths = [
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS', '/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/', '/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
'/Applications/Kindle Previewer 4.app/Contents/Resources/KFXGen/bin/'
] ]
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths + os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
+5 -3
View File
@@ -418,6 +418,7 @@ class WorkerThread(QThread):
if GUI.jobList.item(i).icon().isNull(): if GUI.jobList.item(i).icon().isNull():
currentJobs.append(str(GUI.jobList.item(i).text())) currentJobs.append(str(GUI.jobList.item(i).text()))
GUI.jobList.clear() GUI.jobList.clear()
fusion_cover_path = None
if options.filefusion: if options.filefusion:
bookDir = [] bookDir = []
MW.addMessage.emit('Attempting file fusion', 'info', False) MW.addMessage.emit('Attempting file fusion', 'info', False)
@@ -429,7 +430,8 @@ class WorkerThread(QThread):
if options.output is None: if options.output is None:
options.output = fusion_source_parent options.output = fusion_source_parent
currentJobs.clear() currentJobs.clear()
currentJobs.append(comic2ebook.makeFusion(bookDir)) job, fusion_cover_path = comic2ebook.makeFusion(bookDir)
currentJobs.append(job)
MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False) MW.addMessage.emit('Created fusion at ' + currentJobs[0], 'info', False)
except Exception as e: except Exception as e:
print('Fusion Failed. ' + str(e)) print('Fusion Failed. ' + str(e))
@@ -463,7 +465,7 @@ class WorkerThread(QThread):
jobargv.append(job) jobargv.append(job)
try: try:
comic2ebook.options = comic2ebook.checkOptions(copy(options)) comic2ebook.options = comic2ebook.checkOptions(copy(options))
outputPath = comic2ebook.makeBook(job, self, job_progress_number) outputPath = comic2ebook.makeBook(job, fusion_cover_path, self, job_progress_number)
MW.hideProgressBar.emit() MW.hideProgressBar.emit()
except UserWarning as warn: except UserWarning as warn:
if not self.conversionAlive: if not self.conversionAlive:
@@ -1405,7 +1407,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
except OSError as e: except OSError as e:
self.kindleGen = False self.kindleGen = False
if startup: if startup:
error = f"kindlegen: {e.strerror}\n\n Re-install Rosetta/Kindle Previewer/other Intel app?\n\nPlease email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help" error = f"kindlegen: {e.strerror}\n\n Re-install or re-open Rosetta/Kindle Previewer/other Intel app?"
self.showDialog(error, 'error') self.showDialog(error, 'error')
def __init__(self, kccapp, kccwindow): def __init__(self, kccapp, kccwindow):
+23 -7
View File
@@ -77,9 +77,10 @@ def main(argv=None):
if len(sources) == 0: if len(sources) == 0:
print('No matching files found.') print('No matching files found.')
return 1 return 1
fusion_cover_path = None
if options.filefusion: if options.filefusion:
fusion_source_parent = str(Path(sources[0]).parent) fusion_source_parent = str(Path(sources[0]).parent)
fusion_path = makeFusion(list(sources)) fusion_path, fusion_cover_path = makeFusion(list(sources))
sources.clear() sources.clear()
sources.append(fusion_path) sources.append(fusion_path)
for source in sources: for source in sources:
@@ -89,7 +90,7 @@ def main(argv=None):
options.output = fusion_source_parent options.output = fusion_source_parent
options = checkOptions(options) options = checkOptions(options)
print('Working on ' + source + '...') print('Working on ' + source + '...')
makeBook(source) makeBook(source, fusion_cover_path)
return 0 return 0
@@ -1755,8 +1756,7 @@ def checkTools(source):
sys.exit(1) sys.exit(1)
except OSError as e: except OSError as e:
print(f"kindlegen: {e.strerror}") print(f"kindlegen: {e.strerror}")
print('Re-install Rosetta/Kindle Previewer/other Intel app?') print('Re-install or re-open Rosetta/Kindle Previewer/other Intel app?')
print('Please email Amazon to make Kindle Previewer Apple silicon native at amazon.com/kindle-help')
sys.exit(1) sys.exit(1)
@@ -1773,6 +1773,19 @@ def makeFusion(sources: List[str]):
start = perf_counter() start = perf_counter()
first_path = Path(sources[0]) first_path = Path(sources[0])
fusion_cover_path = None
if first_path.parent.joinpath('Covers').is_dir():
covers = os.listdir(first_path.parent.joinpath('Covers'))
filtered_covers = []
for cover in covers:
_, cover_ext = getImageFileName(cover)
if cover_ext in IMAGE_TYPES:
filtered_covers.append(cover)
try:
fusion_cover_path = first_path.parent.joinpath('Covers', filtered_covers[0])
except IndexError:
pass
if options.tempdir: if options.tempdir:
fusion_parent = first_path.parent fusion_parent = first_path.parent
else: else:
@@ -1814,10 +1827,10 @@ def makeFusion(sources: List[str]):
print(f"makefusion: {end - start} seconds") print(f"makefusion: {end - start} seconds")
print("Combined File: "+ str(fusion_path)) print("Combined File: "+ str(fusion_path))
return str(fusion_path) return str(fusion_path), fusion_cover_path
def makeBook(source, qtgui=None, job_progress=''): def makeBook(source, fusion_cover_path=None, qtgui=None, job_progress=''):
start = perf_counter() start = perf_counter()
global GUI global GUI
GUI = qtgui GUI = qtgui
@@ -1912,9 +1925,12 @@ def makeBook(source, qtgui=None, job_progress=''):
options.customcover = True options.customcover = True
except IndexError: except IndexError:
pass pass
if fusion_cover_path:
options.customcover = True
cover_path = fusion_cover_path
cover = None cover = None
if not options.webtoon: if not options.webtoon or options.customcover:
cover = image.Cover(cover_path, options) cover = image.Cover(cover_path, options)
x, y = image.ProfileData.Profiles[options.profile][1] x, y = image.ProfileData.Profiles[options.profile][1]
+27 -9
View File
@@ -1,4 +1,5 @@
from PIL import ImageOps, ImageFilter, ImageFile from PIL import ImageOps, ImageFilter, ImageFile
from PIL.Image import Image
import numpy as np import numpy as np
from .common_crop import threshold_from_power, group_close_values from .common_crop import threshold_from_power, group_close_values
@@ -149,22 +150,39 @@ def get_bbox_crop_margin(img, power=1, background_color='white'):
return bw_img.getbbox() return bw_img.getbbox()
def ignore_pixels_near_edge(bw_img): def ignore_pixels_near_edge(bw_img: Image):
w, h = bw_img.size w, h = bw_img.size
# going 0.02-0.03 was too much
if int(0.02 * h) == int(0.025 * h):
return
if int(0.02 * w) == int(0.025 * w):
return
edge_bbox = [ edge_bbox = [
(0, 0, w, int(0.02 * h)), (0, 0, w, int(0.02 * h)),
(0, int(0.98 * h), w, h), (0, int(0.98 * h), w, h),
(0, 0, int(0.02 * w), h), (0, 0, int(0.02 * w), h),
(int(0.98 * w), 0, w, h) (int(0.98 * w), 0, w, h)
] ]
for box in edge_bbox:
edge = bw_img.crop(box) inner_bbox = [
h = edge.histogram() (int(0.02 * w), int(0.02 * h), int(0.98 * w), int(0.025 * h)), # top
if not edge.height or not edge.width: (int(0.02 * w), int(0.975 * h), int(0.98 * w), int(0.98 * h)), # lower
continue (int(0.02 * w), int(0.02 * h), int(0.025 * w), int(0.98 * h)), # left
imperfections = h[255] / (edge.height * edge.width) (int(0.975 * w), int(0.02 * h), int(0.98 * w), int(0.98 * h)), # right
if imperfections > 0 and imperfections < .02: ]
bw_img.paste(im=0, box=box)
for edge_box, inner_box in zip(edge_bbox, inner_bbox):
inner_edge = bw_img.crop(inner_box)
h = inner_edge.histogram()
imperfections = h[255] / (inner_edge.height * inner_edge.width)
if imperfections > 0 and imperfections < .001:
bw_img.paste(im=0, box=inner_box)
if imperfections < .001:
edge = bw_img.crop(edge_box)
edge_h = edge.histogram()
if edge_h[-1] != 0:
bw_img.paste(im=0, box=edge_box)
def box_intersect(box1, box2, max_dist): def box_intersect(box1, box2, max_dist):