mirror of
https://github.com/ciromattia/kcc
synced 2026-04-19 07:28:55 +00:00
Compare commits
9 Commits
v7.6.0
...
revert-100
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21249854b9 | ||
|
|
0abf620698 | ||
|
|
69d3bf3278 | ||
|
|
793992f408 | ||
|
|
f41d5327e0 | ||
|
|
6f960aa1d0 | ||
|
|
17c0a73f9f | ||
|
|
1fa5a5b19b | ||
|
|
e8d05c16aa |
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal 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']
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user