mirror of
https://github.com/ciromattia/kcc
synced 2025-12-13 01:36:27 +00:00
* Add rainbow_artifacts_eraser helper This helper file contains the methods necessary to perform a fourier transform on the picture, to remove frequencies responsible for rainbow artifacts on Kaleido screens, and performe the reverse fourier transform * Replace blurring method with frequency removal method to erase rainbow effect on Kaleido 3 screens * High performance improvements by using rfft2 instead of fft2 * Fine-tuned the settings and added the perpendicular direction for a better final rendering The finer settings allow for more information to be retained in the final image, while still effectively removing the rainbow effect. Adding the perpendicular direction results in a better rendering of the final image (avoiding visual artifacts related to suppression at the main angle). * Revert the addition of perpendicular angles and lower attenuation_factor It was a mistake to add the perpendicular angles in the previous commit: I had the rainbow effect removal process called 2 times when I did this, for testing purposes (One before downscale and one after downscale). The proper way to call the process is only after the downscale. And in this case it is not necessary to remove frequencies along the perpendicular angles. In the mean time, attenuation_factor=0.15 has proven to work well along a collection of testing images. It should be my latest commit for this feature * Also attenuate high frequencies at 45° CFA is sometimes orientated at 135°, sometimes at 45° so until we find if there is a law depending on the screen size, e-reader model or something, the best we can do is attenuate high frequencies on those two directions * fix imports * Update comic2ebook.py Calculate is_color with (opt.forcecolor and img.color) pass is_color to img.optimizeForDisplay * Update image.py Remove color check condition, because now we process colored images too. Pass is_color to erase_rainbow_artifacts * Update rainbow_artifacts_eraser.py Add support for colored images: Convert to YUV, extract luminance channel, do FFT -> Filter -> IFFT on luminance channel, insert back to YUV, convert back to RGB To maximize compatibility until we know for sure the orientation of CFA for each device, filtering is now done on 135° + 45° axis After more testing, attenuation_factor is decreased to 0.10 * Update comic2ebook.py Rename rainbow eraser param * Update image.py rename rainbow eraser param * Update KCC.ui Rename rainbow eraser checkbox and tooltip * Update KCC_ui.py Rename erase rainbow checkbox and tooltip * Update KCC_gui.py Rename erase rainbow checkbox and option * Update README.md rename erase rainbow param * Update KCC_gui.py correct param name for eraserainbow --------- Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
503 lines
20 KiB
Python
Executable File
503 lines
20 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (C) 2010 Alex Yatskov
|
|
# Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov <prodoomman@gmail.com>
|
|
# Copyright (c) 2016 Alberto Planas <aplanas@gmail.com>
|
|
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
|
|
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# 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 .rainbow_artifacts_eraser import erase_rainbow_artifacts
|
|
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
|
|
from .inter_panel_crop_alg import crop_empty_inter_panel
|
|
|
|
AUTO_CROP_THRESHOLD = 0.015
|
|
|
|
|
|
class ProfileData:
|
|
def __init__(self):
|
|
pass
|
|
|
|
Palette4 = [
|
|
0x00, 0x00, 0x00,
|
|
0x55, 0x55, 0x55,
|
|
0xaa, 0xaa, 0xaa,
|
|
0xff, 0xff, 0xff
|
|
]
|
|
|
|
Palette15 = [
|
|
0x00, 0x00, 0x00,
|
|
0x11, 0x11, 0x11,
|
|
0x22, 0x22, 0x22,
|
|
0x33, 0x33, 0x33,
|
|
0x44, 0x44, 0x44,
|
|
0x55, 0x55, 0x55,
|
|
0x66, 0x66, 0x66,
|
|
0x77, 0x77, 0x77,
|
|
0x88, 0x88, 0x88,
|
|
0x99, 0x99, 0x99,
|
|
0xaa, 0xaa, 0xaa,
|
|
0xbb, 0xbb, 0xbb,
|
|
0xcc, 0xcc, 0xcc,
|
|
0xdd, 0xdd, 0xdd,
|
|
0xff, 0xff, 0xff,
|
|
]
|
|
|
|
Palette16 = [
|
|
0x00, 0x00, 0x00,
|
|
0x11, 0x11, 0x11,
|
|
0x22, 0x22, 0x22,
|
|
0x33, 0x33, 0x33,
|
|
0x44, 0x44, 0x44,
|
|
0x55, 0x55, 0x55,
|
|
0x66, 0x66, 0x66,
|
|
0x77, 0x77, 0x77,
|
|
0x88, 0x88, 0x88,
|
|
0x99, 0x99, 0x99,
|
|
0xaa, 0xaa, 0xaa,
|
|
0xbb, 0xbb, 0xbb,
|
|
0xcc, 0xcc, 0xcc,
|
|
0xdd, 0xdd, 0xdd,
|
|
0xee, 0xee, 0xee,
|
|
0xff, 0xff, 0xff,
|
|
]
|
|
|
|
PalleteNull = [
|
|
]
|
|
|
|
ProfilesKindleEBOK = {
|
|
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
|
|
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
|
|
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
|
|
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
|
|
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.8),
|
|
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
|
|
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.8),
|
|
}
|
|
|
|
ProfilesKindlePDOC = {
|
|
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.8),
|
|
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.8),
|
|
'KO': ("Kindle Oasis 2/3/Paperwhite 12/Colorsoft 12", (1264, 1680), Palette16, 1.8),
|
|
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
|
|
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
|
|
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
|
|
}
|
|
|
|
ProfilesKindle = {
|
|
**ProfilesKindleEBOK,
|
|
**ProfilesKindlePDOC
|
|
}
|
|
|
|
ProfilesKobo = {
|
|
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
|
|
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
|
|
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
|
|
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
|
|
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
|
|
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
|
|
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
|
|
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
|
|
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
|
|
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.8),
|
|
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
|
|
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.8),
|
|
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
|
|
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
|
|
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
|
|
}
|
|
|
|
ProfilesRemarkable = {
|
|
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.8),
|
|
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.8),
|
|
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.8),
|
|
}
|
|
|
|
Profiles = {
|
|
**ProfilesKindle,
|
|
**ProfilesKobo,
|
|
**ProfilesRemarkable,
|
|
'OTHER': ("Other", (0, 0), Palette16, 1.8),
|
|
}
|
|
|
|
|
|
class ComicPageParser:
|
|
def __init__(self, source, options):
|
|
Image.MAX_IMAGE_PIXELS = int(2048 * 2048 * 2048 // 4 // 3)
|
|
self.opt = options
|
|
self.source = source
|
|
self.size = self.opt.profileData[1]
|
|
self.payload = []
|
|
|
|
# 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.fill = self.fillCheck()
|
|
# backwards compatibility for Pillow >9.1.0
|
|
if not hasattr(Image, 'Resampling'):
|
|
Image.Resampling = Image
|
|
self.splitCheck()
|
|
|
|
def getImageHistogram(self, image):
|
|
histogram = image.histogram()
|
|
if histogram[0] == 0:
|
|
return -1
|
|
elif histogram[255] == 0:
|
|
return 1
|
|
else:
|
|
return 0
|
|
|
|
def splitCheck(self):
|
|
width, height = self.image.size
|
|
dstwidth, dstheight = self.size
|
|
if self.opt.maximizestrips:
|
|
leftbox = (0, 0, int(width / 2), height)
|
|
rightbox = (int(width / 2), 0, width, height)
|
|
if self.opt.righttoleft:
|
|
pageone = self.image.crop(rightbox)
|
|
pagetwo = self.image.crop(leftbox)
|
|
else:
|
|
pageone = self.image.crop(leftbox)
|
|
pagetwo = self.image.crop(rightbox)
|
|
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.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.fill])
|
|
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
|
if self.opt.splitter != 1:
|
|
if width > height:
|
|
leftbox = (0, 0, int(width / 2), height)
|
|
rightbox = (int(width / 2), 0, width, height)
|
|
else:
|
|
leftbox = (0, 0, width, int(height / 2))
|
|
rightbox = (0, int(height / 2), width, height)
|
|
if self.opt.righttoleft:
|
|
pageone = self.image.crop(rightbox)
|
|
pagetwo = self.image.crop(leftbox)
|
|
else:
|
|
pageone = self.image.crop(leftbox)
|
|
pagetwo = self.image.crop(rightbox)
|
|
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.fill])
|
|
else:
|
|
self.payload.append(['N', self.source, self.image, self.fill])
|
|
|
|
def fillCheck(self):
|
|
if self.opt.bordersColor:
|
|
return self.opt.bordersColor
|
|
else:
|
|
bw = self.image.convert('L').point(lambda x: 0 if x < 128 else 255, '1')
|
|
imageBoxA = bw.getbbox()
|
|
imageBoxB = ImageChops.invert(bw).getbbox()
|
|
if imageBoxA is None or imageBoxB is None:
|
|
surfaceB, surfaceW = 0, 0
|
|
diff = 0
|
|
else:
|
|
surfaceB = (imageBoxA[2] - imageBoxA[0]) * (imageBoxA[3] - imageBoxA[1])
|
|
surfaceW = (imageBoxB[2] - imageBoxB[0]) * (imageBoxB[3] - imageBoxB[1])
|
|
diff = ((max(surfaceB, surfaceW) - min(surfaceB, surfaceW)) / min(surfaceB, surfaceW)) * 100
|
|
if diff > 0.5:
|
|
if surfaceW < surfaceB:
|
|
return 'white'
|
|
elif surfaceW > surfaceB:
|
|
return 'black'
|
|
else:
|
|
fill = 0
|
|
startY = 0
|
|
while startY < bw.size[1]:
|
|
if startY + 5 > bw.size[1]:
|
|
startY = bw.size[1] - 5
|
|
fill += self.getImageHistogram(bw.crop((0, startY, bw.size[0], startY + 5)))
|
|
startY += 5
|
|
startX = 0
|
|
while startX < bw.size[0]:
|
|
if startX + 5 > bw.size[0]:
|
|
startX = bw.size[0] - 5
|
|
fill += self.getImageHistogram(bw.crop((startX, 0, startX + 5, bw.size[1])))
|
|
startX += 5
|
|
if fill > 0:
|
|
return 'black'
|
|
else:
|
|
return 'white'
|
|
|
|
|
|
class ComicPage:
|
|
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.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])
|
|
self.targetPathStart = os.path.join(path[0], os.path.splitext(path[1])[0])
|
|
if 'N' in mode:
|
|
self.targetPathOrder = '-kcc-x'
|
|
elif 'R' in mode:
|
|
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
|
|
if not options.norotate:
|
|
self.rotated = True
|
|
elif 'S1' in mode:
|
|
self.targetPathOrder = '-kcc-b'
|
|
elif 'S2' in mode:
|
|
self.targetPathOrder = '-kcc-c'
|
|
# backwards compatibility for Pillow >9.1.0
|
|
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 = []
|
|
if self.rotated:
|
|
flags.append('Rotated')
|
|
if self.fill != 'white':
|
|
flags.append('BlackBackground')
|
|
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
|
|
w, h = self.image.size
|
|
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
|
|
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
|
|
elif self.opt.kindle_scribe_azw3:
|
|
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
|
|
else:
|
|
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
|
|
if os.path.isfile(self.orgPath):
|
|
os.remove(self.orgPath)
|
|
return [Path(targetPath).name, flags]
|
|
except IOError as err:
|
|
raise RuntimeError('Cannot save image. ' + str(err))
|
|
|
|
def save_with_codec(self, image, targetPath):
|
|
if self.opt.forcepng:
|
|
image.info["transparency"] = None
|
|
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
|
|
targetPath += '.gif'
|
|
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
|
else:
|
|
targetPath += '.png'
|
|
image.save(targetPath, 'PNG', optimize=1)
|
|
else:
|
|
targetPath += '.jpg'
|
|
if self.opt.mozjpeg:
|
|
with io.BytesIO() as output:
|
|
image.save(output, format="JPEG", optimize=1, quality=85)
|
|
input_jpeg_bytes = output.getvalue()
|
|
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
|
|
with open(targetPath, "wb") as output_jpeg_file:
|
|
output_jpeg_file.write(output_jpeg_bytes)
|
|
else:
|
|
image.save(targetPath, 'JPEG', optimize=1, quality=85)
|
|
return targetPath
|
|
|
|
def gammaCorrectImage(self):
|
|
gamma = self.opt.gamma
|
|
if gamma < 0.1:
|
|
gamma = self.gamma
|
|
if self.gamma != 1.0 and self.color:
|
|
gamma = 1.0
|
|
if gamma == 1.0:
|
|
pass
|
|
else:
|
|
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
|
|
|
|
def autocontrastImage(self):
|
|
if self.opt.autolevel and not self.color:
|
|
self.convertToGrayscale()
|
|
h = self.image.histogram()
|
|
most_common_dark_pixel_count = max(h[:64])
|
|
black_point = h.index(most_common_dark_pixel_count)
|
|
bp = black_point
|
|
self.image = self.image.point(lambda p: p if p > bp else bp)
|
|
|
|
# don't autocontrast grayscale pages that were originally color
|
|
if not self.opt.forcecolor and self.color:
|
|
return
|
|
|
|
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
|
|
|
def convertToGrayscale(self):
|
|
self.image = self.image.convert('L')
|
|
|
|
def quantizeImage(self):
|
|
# remove all color pixels from image, since colorCheck() has some tolerance
|
|
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
|
|
self.image = self.image.convert("RGB")
|
|
|
|
palImg = Image.new('P', (1, 1))
|
|
palImg.putpalette(self.palette)
|
|
self.image = self.image.quantize(palette=palImg)
|
|
|
|
def optimizeForDisplay(self, eraserainbow, is_color):
|
|
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
|
|
if eraserainbow:
|
|
self.image = erase_rainbow_artifacts(self.image, is_color)
|
|
|
|
def resizeImage(self):
|
|
ratio_device = float(self.size[1]) / float(self.size[0])
|
|
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
|
|
method = self.resize_method()
|
|
if self.opt.stretch:
|
|
self.image = self.image.resize(self.size, method)
|
|
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
|
pass
|
|
else: # if image bigger than device resolution or smaller with upscaling
|
|
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
|
self.image = ImageOps.fit(self.image, self.size, method=method)
|
|
elif (self.opt.format == 'CBZ' or self.opt.kfx) and not self.opt.white_borders:
|
|
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
|
else:
|
|
self.image = ImageOps.contain(self.image, self.size, method=method)
|
|
|
|
def resize_method(self):
|
|
if self.image.size[0] < self.size[0] and self.image.size[1] < self.size[1]:
|
|
return Image.Resampling.BICUBIC
|
|
else:
|
|
return Image.Resampling.LANCZOS
|
|
|
|
def maybeCrop(self, box, minimum):
|
|
w, h = self.image.size
|
|
left, upper, right, lower = box
|
|
if self.opt.preservemargin:
|
|
ratio = 1 - self.opt.preservemargin / 100
|
|
box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio)
|
|
box_area = (box[2] - box[0]) * (box[3] - box[1])
|
|
image_area = self.image.size[0] * self.image.size[1]
|
|
if (box_area / image_area) >= minimum:
|
|
self.image = self.image.crop(box)
|
|
|
|
def cropPageNumber(self, power, minimum):
|
|
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
|
|
|
|
if bbox:
|
|
self.maybeCrop(bbox, minimum)
|
|
|
|
def cropMargin(self, power, minimum):
|
|
bbox = get_bbox_crop_margin(self.image, power, self.fill)
|
|
|
|
if bbox:
|
|
self.maybeCrop(bbox, minimum)
|
|
|
|
def cropInterPanelEmptySections(self, direction):
|
|
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
|
|
|
|
class Cover:
|
|
def __init__(self, source, opt):
|
|
self.options = opt
|
|
self.source = source
|
|
self.image = Image.open(source)
|
|
# backwards compatibility for Pillow >9.1.0
|
|
if not hasattr(Image, 'Resampling'):
|
|
Image.Resampling = Image
|
|
self.process()
|
|
|
|
def process(self):
|
|
self.image = self.image.convert('RGB')
|
|
self.image = ImageOps.autocontrast(self.image)
|
|
if not self.options.forcecolor:
|
|
self.image = self.image.convert('L')
|
|
self.crop_main_cover()
|
|
|
|
size = list(self.options.profileData[1])
|
|
if self.options.kindle_scribe_azw3:
|
|
size[1] = min(size[1], 1920)
|
|
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
|
|
|
|
def crop_main_cover(self):
|
|
w, h = self.image.size
|
|
if w / h > 2:
|
|
if self.options.righttoleft:
|
|
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
|
else:
|
|
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
|
elif w / h > 1.3:
|
|
if self.options.righttoleft:
|
|
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
|
else:
|
|
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
|
|
|
def save_to_epub(self, target, tomeid, len_tomes=0):
|
|
try:
|
|
if tomeid == 0:
|
|
self.image.save(target, "JPEG", optimize=1, quality=85)
|
|
else:
|
|
copy = self.image.copy()
|
|
draw = ImageDraw.Draw(copy)
|
|
w, h = copy.size
|
|
draw.text(
|
|
xy=(w/2, h * .85),
|
|
text=f'{tomeid}/{len_tomes}',
|
|
anchor='ms',
|
|
font_size=h//7,
|
|
fill=255,
|
|
stroke_fill=0,
|
|
stroke_width=25
|
|
)
|
|
copy.save(target, "JPEG", optimize=1, quality=85)
|
|
dot_cover = Path(target).with_stem('._' + Path(target).stem)
|
|
if os.path.exists(dot_cover):
|
|
os.remove(dot_cover)
|
|
except IOError:
|
|
raise RuntimeError('Failed to save cover.')
|
|
|
|
def saveToKindle(self, kindle, asin):
|
|
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
|
|
try:
|
|
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
|
|
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
|
|
except IOError:
|
|
raise RuntimeError('Failed to upload cover.')
|