1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-19 07:28:55 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Alex Xu
21249854b9 Revert "upgrade 7z to 7zz (#1005)"
This reverts commit 17c0a73f9f.
2025-07-03 12:24:54 -07:00
Alex Xu
0abf620698 Create FUNDING.yml 2025-07-03 11:42:46 -07:00
Alex Xu
69d3bf3278 simplify removeNonImages (#1009) 2025-07-02 17:28:03 -07:00
Alex Xu
793992f408 bump to 8.0.0 2025-07-02 10:18:30 -07:00
Alex Xu
f41d5327e0 remove non images early (#1007) 2025-07-02 10:17:54 -07:00
Alex Xu
6f960aa1d0 bump mozjpeg 2025-07-01 08:12:32 -07:00
Alex Xu
17c0a73f9f upgrade 7z to 7zz (#1005) 2025-07-01 08:12:01 -07:00
Adrian
1fa5a5b19b Improved color detection (#1003)
* Improved color detection

* use pure python

---------

Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
2025-06-30 17:18:04 -07:00
Alex Xu
e8d05c16aa Update README.md 2025-06-29 14:03:43 -07:00
7 changed files with 71 additions and 63 deletions

15
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: eink_dude
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -27,7 +27,7 @@ on underpowered ereaders with small 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:
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 (feature in progress)
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

View File

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

View File

@@ -647,7 +647,7 @@ def imgFileProcessing(work):
img.autocontrastImage()
img.resizeImage()
img.optimizeForDisplay(opt.reducerainbow)
if opt.forcecolor and workImg.color:
if opt.forcecolor and img.color:
pass
elif opt.forcepng:
img.quantizeImage()
@@ -690,16 +690,6 @@ def getWorkFolder(afile):
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
path = os.path.join(workdir, tdir[0])
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
@@ -822,6 +812,22 @@ def getPanelViewSize(deviceres, size):
return str(int(x)), str(int(y))
def removeNonImages(filetree):
# clean dot from original file
dot_clean(filetree)
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2'):
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
# remove empty nested folders
for root, dirs, files in os.walk(filetree, False):
if not files and not dirs:
os.rmdir(root)
def sanitizeTree(filetree):
chapterNames = {}
page = 1
@@ -830,13 +836,13 @@ def sanitizeTree(filetree):
dirs.sort(key=OS_SORT_KEY)
files.sort(key=OS_SORT_KEY)
for name in files:
splitname = os.path.splitext(name)
_, ext = getImageFileName(name)
# 9999 page limit
slugified = f'kcc-{page:04}'
page += 1
newKey = os.path.join(root, slugified + splitname[1])
newKey = os.path.join(root, slugified + ext)
key = os.path.join(root, name)
if key != newKey:
os.replace(key, newKey)
@@ -870,8 +876,7 @@ def sanitizePermissions(filetree):
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD)
for name in dirs:
os.chmod(os.path.join(root, name), S_IWRITE | S_IREAD | S_IEXEC)
# clean dot from original file
dot_clean(filetree)
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
@@ -990,10 +995,6 @@ def detectSuboptimalProcessing(tmppath, orgpath):
os.remove(os.path.join(root, name))
except OSError as e:
raise RuntimeError(f"{name}: {e}")
# remove empty nested folders
for root, dirs, files in os.walk(tmppath, False):
if not files and not dirs:
os.rmdir(root)
if alreadyProcessed:
print("WARNING: Source files are probably created by KCC. The second conversion will decrease quality.")
if GUI:
@@ -1309,6 +1310,7 @@ def makeBook(source, qtgui=None):
path = getWorkFolder(source)
print("Checking images...")
getComicInfo(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
chapterNames, cover_path = sanitizeTree(os.path.join(path, 'OEBPS', 'Images'))
cover = image.Cover(cover_path, options)

View File

@@ -20,7 +20,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter, ImageDraw
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
@@ -146,11 +148,9 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
self.image = Image.open(srcImgPath)
self.image.verify()
self.image = Image.open(srcImgPath).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -181,13 +181,13 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.color, self.fill])
self.payload.append(['N', self.source, new_image, self.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
if width > height:
@@ -202,38 +202,15 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread,
self.color, self.fill])
self.payload.append(['R', self.source, spread, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.color, self.fill])
def colorCheck(self):
if self.opt.webtoon:
return True
else:
img = self.image.copy()
bands = img.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
thumb = img.resize((40, 40))
SSE, bias = 0, [0, 0, 0]
bias = ImageStat.Stat(thumb).mean[:3]
bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata():
mu = sum(pixel) / 3
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
MSE = float(SSE) / (40 * 40)
if MSE > 22:
return True
else:
return False
else:
return False
self.payload.append(['N', self.source, self.image, self.fill])
def fillCheck(self):
if self.opt.bordersColor:
@@ -275,14 +252,14 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, color, fill):
def __init__(self, options, mode, path, image, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.image = image
self.color = color
self.original_color_mode = image.mode
self.image = image.convert("RGB")
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
@@ -301,6 +278,26 @@ class ComicPage:
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@cached_property
def color(self):
if self.original_color_mode in ("L", "1"):
return False
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist = cb.histogram()
cr_hist = cr.histogram()
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0] if len(cb_nonzero) else 0
cr_spread = cr_nonzero[-1] - cr_nonzero[0] if len(cr_nonzero) else 0
SPREAD_THRESHOLD=20
if cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return False
else:
return True
def saveToDir(self):
try:
flags = []

View File

@@ -49,12 +49,6 @@ class HTMLStripper(HTMLParser):
def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile)
ext = ext.lower()
if (name.startswith('.') and len(name) == 1):
return None
if name.startswith('._'):
return None
if ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.j2k', '.jpx']:
return None
return [name, ext]

View File

@@ -5,7 +5,7 @@ requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization==1.2.0
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4