# -*- coding: utf-8 -*- # # Copyright (C) 2010 Alex Yatskov # Copyright (C) 2011 Stanislav (proDOOMman) Kosolapov # Copyright (c) 2016 Alberto Planas # Copyright (c) 2012-2014 Ciro Mattia Gonano # Copyright (c) 2013-2019 Pawel Jastrzebski # # 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 . import os from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter from .shared import md5Checksum 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 = [ ] Profiles = { 'K1': ("Kindle 1", (600, 670), Palette4, 1.8), 'K2': ("Kindle 2", (600, 670), Palette15, 1.8), 'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8), 'K578': ("Kindle", (600, 800), Palette16, 1.8), 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8), 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8), 'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8), 'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.8), '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), 'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8), '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 = [] self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB') self.color = self.colorCheck() self.fill = self.fillCheck() 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 (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \ and not self.opt.webtoon and self.opt.splitter == 1: self.payload.append(['R', self.source, self.image.rotate(90, Image.BICUBIC, True), self.color, 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.color, self.fill]) self.payload.append(['S2', self.source, pagetwo, self.color, self.fill]) if self.opt.splitter > 0: self.payload.append(['R', self.source, self.image.rotate(90, Image.BICUBIC, True), self.color, 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 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, color, 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.image = image self.color = color self.fill = fill self.rotated = False self.orgPath = os.path.join(path[0], path[1]) if 'N' in mode: self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC' elif 'R' in mode: self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A' self.rotated = True elif 'S1' in mode: self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-B' elif 'S2' in mode: self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-C' def saveToDir(self): try: flags = [] if not self.opt.forcecolor and not self.opt.forcepng: self.image = self.image.convert('L') if self.rotated: flags.append('Rotated') if self.fill != 'white': flags.append('BlackBackground') if self.opt.forcepng: self.targetPath += '.png' self.image.save(self.targetPath, 'PNG', optimize=1) else: self.targetPath += '.jpg' self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85) return [md5Checksum(self.targetPath), flags, self.orgPath] except IOError as err: raise RuntimeError('Cannot save image. ' + str(err)) def autocontrastImage(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: self.image = ImageOps.autocontrast(self.image) else: self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))) def quantizeImage(self): colors = len(self.palette) // 3 if colors < 256: self.palette += self.palette[:3] * (256 - colors) palImg = Image.new('P', (1, 1)) palImg.putpalette(self.palette) self.image = self.image.convert('L') self.image = self.image.convert('RGB') # Quantize is deprecated but new function call it internally anyway... self.image = self.image.quantize(palette=palImg) def resizeImage(self): if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]: method = Image.BICUBIC else: method = Image.LANCZOS if self.opt.stretch or (self.opt.kfx and ('-KCC-B' in self.targetPath or '-KCC-C' in self.targetPath)): self.image = self.image.resize(self.size, method) elif self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1] and not self.opt.upscale: if self.opt.format == 'CBZ' or self.opt.kfx: borderw = int((self.size[0] - self.image.size[0]) / 2) borderh = int((self.size[1] - self.image.size[1]) / 2) self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill) if self.image.size[0] != self.size[0] or self.image.size[1] != self.size[1]: self.image = ImageOps.fit(self.image, self.size, method=Image.BICUBIC, centering=(0.5, 0.5)) else: if self.opt.format == 'CBZ' or self.opt.kfx: ratioDev = float(self.size[0]) / float(self.size[1]) if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev: diff = int(self.image.size[1] * ratioDev) - self.image.size[0] self.image = ImageOps.expand(self.image, border=(int(diff / 2), 0), fill=self.fill) elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev: diff = int(self.image.size[0] / ratioDev) - self.image.size[1] self.image = ImageOps.expand(self.image, border=(0, int(diff / 2)), fill=self.fill) self.image = ImageOps.fit(self.image, self.size, method=method, centering=(0.5, 0.5)) else: hpercent = self.size[1] / float(self.image.size[1]) wsize = int((float(self.image.size[0]) * float(hpercent))) self.image = self.image.resize((wsize, self.size[1]), method) if self.image.size[0] > self.size[0] or self.image.size[1] > self.size[1]: self.image.thumbnail(self.size, Image.LANCZOS) def getBoundingBox(self, tmptmg): min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size] max_margin = [int(0.1 * i + 0.5) for i in tmptmg.size] bbox = tmptmg.getbbox() bbox = ( max(0, min(max_margin[0], bbox[0] - min_margin[0])), max(0, min(max_margin[1], bbox[1] - min_margin[1])), min(tmptmg.size[0], max(tmptmg.size[0] - max_margin[0], bbox[2] + min_margin[0])), min(tmptmg.size[1], max(tmptmg.size[1] - max_margin[1], bbox[3] + min_margin[1])), ) return bbox def cropPageNumber(self, power): if self.fill != 'white': tmptmg = self.image.convert(mode='L') else: tmptmg = ImageOps.invert(self.image.convert(mode='L')) tmptmg = tmptmg.point(lambda x: x and 255) tmptmg = tmptmg.filter(ImageFilter.MinFilter(size=3)) tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=5)) tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x) self.image = self.image.crop(tmptmg.getbbox()) if tmptmg.getbbox() else self.image def cropMargin(self, power): if self.fill != 'white': tmptmg = self.image.convert(mode='L') else: tmptmg = ImageOps.invert(self.image.convert(mode='L')) tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=3)) tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x) self.image = self.image.crop(self.getBoundingBox(tmptmg)) if tmptmg.getbbox() else self.image class Cover: def __init__(self, source, target, opt, tomeid): self.options = opt self.source = source self.target = target if tomeid == 0: self.tomeid = 1 else: self.tomeid = tomeid self.image = Image.open(source) 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.image.thumbnail(self.options.profileData[1], Image.LANCZOS) self.save() def save(self): try: self.image.save(self.target, "JPEG", optimize=1, quality=85) except IOError: raise RuntimeError('Failed to save cover.') def saveToKindle(self, kindle, asin): self.image = self.image.resize((300, 470), Image.ANTIALIAS) 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.')