diff --git a/README.md b/README.md index f700680b..cfaf2145 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,11 @@ Usage: kcc-c2e [options] comic_file|comic_folder Options: MAIN: -p PROFILE, --profile=PROFILE - Device profile (Available options: K1, K2, K3, K45, KDX, - KPW, KV, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO) [Default=KV] + Device profile (Available options: K1, K2, K3, K45, + KDX, KPW, KV, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, + KoAO) [Default=KV] -m, --manga-style Manga style (right-to-left reading and splitting) + -2, --two-panel Display two not four panels in Panel View mode -w, --webtoon Webtoon processing mode OUTPUT SETTINGS: @@ -86,29 +88,28 @@ Options: -t TITLE, --title=TITLE Comic title [Default=filename or directory name] -f FORMAT, --format=FORMAT - Output format (Available options: Auto, MOBI, EPUB, CBZ) - [Default=Auto] + Output format (Available options: Auto, MOBI, EPUB, + CBZ) [Default=Auto] -b, --batchsplit Split output into multiple files PROCESSING: -u, --upscale Resize images smaller than device's resolution -s, --stretch Stretch images to device's resolution -r SPLITTER, --splitter=SPLITTER - Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0] + Double page parsing mode. 0: Split 1: Rotate 2: Both + [Default=0] -g GAMMA, --gamma=GAMMA - Apply gamma correction to linearize the image [Default=Auto] - --hq Enable high quality Panel View + Apply gamma correction to linearize the image + [Default=Auto] + -c CROPPING, --cropping=CROPPING + Set cropping mode. 0: Disabled 1: Margins 2: Margins + + page numbers [Default=2] + --cp=CROPPINGP, --croppingpower=CROPPINGP + Set cropping power [Default=1.0] --blackborders Disable autodetection and force black borders --whiteborders Disable autodetection and force white borders --forcecolor Don't convert images to grayscale --forcepng Create PNG files instead JPEG - --cropping=CROPPING - Set cropping mode. 0: Disabled 1: Margins 2: Margins + - page numbers [Default=2] - --croppingpower=CROPPINGP - Set margin cropping threshold [Default=0.1] - --croppingpowerpage=CROPPINGPN - Set page number cropping threshold [Default=5.0] CUSTOM PROFILE: --customwidth=CUSTOMWIDTH @@ -159,6 +160,12 @@ The app relies and includes the following scripts: * [Kobo Aura ONE](http://kcc.iosphe.re/Samples/Ubunchu-KoAO.kepub.epub) ## CHANGELOG +####5.2: +* Added new Panel View options +* Implemented new margin detection algorithm +* Removed HQ Panel View mode +* Fixed multiple smaller issues + ####5.1.3: * Added Kobo Aura ONE profile * Fixed few small bugs diff --git a/gui/KCC.ui b/gui/KCC.ui index eb8b84e4..3bdd9173 100644 --- a/gui/KCC.ui +++ b/gui/KCC.ui @@ -174,10 +174,10 @@ - <html><head/><body><p style='white-space:pre'>High quality Panel View.<br/>Require source files with bigger resolution than target device.<br/><span style=" font-weight:600;">Highly impact size of output file!</span></p></body></html> + <html><head/><body><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Unchecked - 4 panels<br/></span>Zoom each corner separately.</p><p style=\'white-space:pre\'><span style=\" font-weight:600; text-decoration: underline;\">Checked - 2 panels<br/></span>Zoom only the top and bottom of the page.</p></body></html> - HQ zoom + Panel View 4/2 diff --git a/kcc.iss b/kcc.iss index d536ab25..28a73176 100644 --- a/kcc.iss +++ b/kcc.iss @@ -1,5 +1,5 @@ #define MyAppName "Kindle Comic Converter" -#define MyAppVersion "5.1.3" +#define MyAppVersion "5.2" #define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski" #define MyAppURL "http://kcc.iosphe.re/" #define MyAppExeName "KCC.exe" diff --git a/kcc/KCC_gui.py b/kcc/KCC_gui.py index c7735290..dcc9e1bc 100644 --- a/kcc/KCC_gui.py +++ b/kcc/KCC_gui.py @@ -33,7 +33,7 @@ from xml.sax.saxutils import escape from platform import platform from raven import Client -from .shared import md5Checksum, HTMLStripper, sanitizeTrace +from .shared import md5Checksum, HTMLStripper, sanitizeTrace, saferRemove from . import __version__ from . import comic2ebook from . import metadata @@ -257,7 +257,7 @@ def run(self): elif GUI.rotateBox.checkState() == 2: options.splitter = 1 if GUI.qualityBox.isChecked(): - options.hqmode = True + options.autoscale = True if GUI.webtoonBox.isChecked(): options.webtoon = True if GUI.upscaleBox.checkState() == 1: @@ -331,7 +331,7 @@ def run(self): if 'outputPath' in locals(): for item in outputPath: if os.path.exists(item): - os.remove(item) + saferRemove(item) self.clean() return if not self.errors: @@ -358,9 +358,9 @@ def run(self): if not self.conversionAlive: for item in outputPath: if os.path.exists(item): - os.remove(item) + saferRemove(item) if os.path.exists(item.replace('.epub', '.mobi')): - os.remove(item.replace('.epub', '.mobi')) + saferRemove(item.replace('.epub', '.mobi')) self.clean() return if self.kindlegenErrorCode[0] == 0: @@ -381,7 +381,7 @@ def run(self): for item in outputPath: GUI.progress.content = '' mobiPath = item.replace('.epub', '.mobi') - os.remove(mobiPath + '_toclean') + saferRemove(mobiPath + '_toclean') if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(mobiPath): try: move(mobiPath, GUI.targetDirectory) @@ -393,15 +393,15 @@ def run(self): for item in outputPath: comic2ebook.options.covers[outputPath.index(item)][0].saveToKindle( k, comic2ebook.options.covers[outputPath.index(item)][1]) - MW.addMessage.emit('Kindle detected. Uploading covers...', 'info', False) + MW.addMessage.emit('Kindle detected. Uploading covers... Done!', 'info', False) else: GUI.progress.content = '' for item in outputPath: mobiPath = item.replace('.epub', '.mobi') if os.path.exists(mobiPath): - os.remove(mobiPath) + saferRemove(mobiPath) if os.path.exists(mobiPath + '_toclean'): - os.remove(mobiPath + '_toclean') + saferRemove(mobiPath + '_toclean') MW.addMessage.emit('Failed to process MOBI file!', 'error', False) MW.addTrayMessage.emit('Failed to process MOBI file!', 'Critical') else: @@ -409,9 +409,9 @@ def run(self): epubSize = (os.path.getsize(self.kindlegenErrorCode[2])) // 1024 // 1024 for item in outputPath: if os.path.exists(item): - os.remove(item) + saferRemove(item) if os.path.exists(item.replace('.epub', '.mobi')): - os.remove(item.replace('.epub', '.mobi')) + saferRemove(item.replace('.epub', '.mobi')) MW.addMessage.emit('KindleGen failed to create MOBI!', 'error', False) MW.addTrayMessage.emit('KindleGen failed to create MOBI!', 'Critical') if self.kindlegenErrorCode[0] == 1 and self.kindlegenErrorCode[1] != '': @@ -603,7 +603,9 @@ def togglewebtoonBox(self, value): GUI.upscaleBox.setEnabled(False) GUI.upscaleBox.setChecked(True) else: - GUI.qualityBox.setEnabled(True) + profile = GUI.profiles[str(GUI.deviceBox.currentText())] + if profile['PVOptions']: + GUI.qualityBox.setEnabled(True) GUI.mangaBox.setEnabled(True) GUI.rotateBox.setEnabled(True) GUI.upscaleBox.setEnabled(True) @@ -629,9 +631,9 @@ def changeDevice(self): self.changeFormat() GUI.gammaSlider.setValue(0) self.changeGamma(0) - GUI.qualityBox.setEnabled(profile['Quality']) + GUI.qualityBox.setEnabled(profile['PVOptions']) GUI.upscaleBox.setChecked(profile['DefaultUpscale']) - if not profile['Quality']: + if not profile['PVOptions']: GUI.qualityBox.setChecked(False) if str(GUI.deviceBox.currentText()) == 'Other': self.addMessage('' @@ -643,7 +645,7 @@ def changeFormat(self, outputFormat=None): GUI.formatBox.setCurrentIndex(outputFormat) else: GUI.formatBox.setCurrentIndex(profile['DefaultFormat']) - GUI.qualityBox.setEnabled(profile['Quality']) + GUI.qualityBox.setEnabled(profile['PVOptions']) def stripTags(self, html): s = HTMLStripper() @@ -894,39 +896,39 @@ def __init__(self, KCCAplication, KCCWindow): MW.resize(500, 500) self.profiles = { - "Kindle Oasis": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle Oasis": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'Label': 'KV'}, - "Kindle Voyage": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle Voyage": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'Label': 'KV'}, - "Kindle PW 3": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle PW 3": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': True, 'Label': 'KV'}, - "Kindle PW 1/2": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle PW 1/2": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'KPW'}, - "Kindle": {'Quality': True, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle": {'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'K45'}, - "Kindle DX/DXG": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 2, + "Kindle DX/DXG": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 2, 'DefaultUpscale': False, 'Label': 'KDX'}, - "Kobo Mini/Touch": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Mini/Touch": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': False, 'Label': 'KoMT'}, - "Kobo Glo": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Glo": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': False, 'Label': 'KoG'}, - "Kobo Glo HD": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Glo HD": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': False, 'Label': 'KoGHD'}, - "Kobo Aura": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Aura": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': False, 'Label': 'KoA'}, - "Kobo Aura HD": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Aura HD": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'Label': 'KoAHD'}, - "Kobo Aura H2O": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Aura H2O": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'Label': 'KoAH2O'}, - "Kobo Aura ONE": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 1, + "Kobo Aura ONE": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'Label': 'KoAO'}, - "Other": {'Quality': False, 'ForceExpert': True, 'DefaultFormat': 1, + "Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'Label': 'OTHER'}, - "Kindle 1": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'K1'}, - "Kindle 2": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'K2'}, - "Kindle 3": {'Quality': False, 'ForceExpert': False, 'DefaultFormat': 0, + "Kindle 3": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'Label': 'K3'}, } profilesGUI = [ diff --git a/kcc/KCC_ui.py b/kcc/KCC_ui.py index 5418e3ee..5e718ef2 100644 --- a/kcc/KCC_ui.py +++ b/kcc/KCC_ui.py @@ -241,8 +241,8 @@ def retranslateUi(self, mainWindow): self.mangaBox.setText(_translate("mainWindow", "Manga mode")) self.rotateBox.setToolTip(_translate("mainWindow", "

Unchecked - Split
Double page spreads will be cut into two separate pages.

Indeterminate - Rotate and split
Double page spreads will be displayed twice. First rotated and then split.

Checked - Rotate
Double page spreads will be rotated.

")) self.rotateBox.setText(_translate("mainWindow", "Spread splitter")) - self.qualityBox.setToolTip(_translate("mainWindow", "

High quality Panel View.
Require source files with bigger resolution than target device.
Highly impact size of output file!

")) - self.qualityBox.setText(_translate("mainWindow", "HQ zoom")) + self.qualityBox.setToolTip(_translate("mainWindow", "

Unchecked - 4 panels
Zoom each corner separately.

Checked - 2 panels
Zoom only the top and bottom of the page.

")) + self.qualityBox.setText(_translate("mainWindow", "Panel View 4/2")) self.webtoonBox.setToolTip(_translate("mainWindow", "

Enable special parsing mode for Korean Webtoons.

")) self.webtoonBox.setText(_translate("mainWindow", "Webtoon mode")) self.upscaleBox.setToolTip(_translate("mainWindow", "

Unchecked - Nothing
Images smaller than device resolution will not be resized.

Indeterminate - Stretching
Images smaller than device resolution will be resized. Aspect ratio will be not preserved.

Checked - Upscaling
Images smaller than device resolution will be resized. Aspect ratio will be preserved.

")) diff --git a/kcc/__init__.py b/kcc/__init__.py index 5382553c..d4f4bd0a 100644 --- a/kcc/__init__.py +++ b/kcc/__init__.py @@ -1,4 +1,4 @@ -__version__ = '5.1.3' +__version__ = '5.2' __license__ = 'ISC' __copyright__ = '2012-2016, Ciro Mattia Gonano , Pawel Jastrzebski ' __docformat__ = 'restructuredtext en' diff --git a/kcc/cbxarchive.py b/kcc/cbxarchive.py index 569dad56..3c857e77 100644 --- a/kcc/cbxarchive.py +++ b/kcc/cbxarchive.py @@ -27,7 +27,7 @@ except ImportError: walk = os.walk from . import rarfile -from .shared import check7ZFile as is_7zfile, saferReplace +from .shared import check7ZFile as is_7zfile, saferReplace, saferRemove class CBxArchive: @@ -66,7 +66,7 @@ def extractCBR(self, targetdir): for root, dirnames, filenames in walk(targetdir): for filename in filenames: if filename.startswith('__MACOSX') or filename.endswith('.DS_Store') or filename.endswith('humbs.db'): - os.remove(os.path.join(root, filename)) + saferRemove(os.path.join(root, filename)) def extractCB7(self, targetdir): # Workaround for some wide UTF-8 + Popen abnormalities @@ -80,7 +80,7 @@ def extractCB7(self, targetdir): if b"Everything is Ok" in line: extracted = True if sys.platform.startswith('darwin'): - os.remove(self.origFileName) + saferRemove(self.origFileName) if not extracted: raise OSError diff --git a/kcc/comic2ebook.py b/kcc/comic2ebook.py index a3037394..da9c906f 100755 --- a/kcc/comic2ebook.py +++ b/kcc/comic2ebook.py @@ -95,8 +95,6 @@ def buildHTML(path, imgfile, imgfilepath): additionalStyle = 'background-color:#FFFFFF;' htmlpath = '' postfix = '' - size = '' - imgfilepv = '' backref = 1 head = path while True: @@ -122,21 +120,16 @@ def buildHTML(path, imgfile, imgfilepath): "\n"]) if options.iskindle and options.panelview: - if options.hqmode: - imgfilepv = list(os.path.splitext(imgfile)) - imgfilepv[0] += "-hq" - imgfilepv = "".join(imgfilepv) - if os.path.isfile(os.path.join(head, "Images", postfix, imgfilepv)): - size = Image.open(os.path.join(head, "Images", postfix, imgfilepv)).size - if not options.hqmode or not size: - imgfilepv = imgfile - sizeTmp = Image.open(os.path.join(head, "Images", postfix, imgfilepv)).size + sizeTmp = Image.open(os.path.join(head, "Images", postfix, imgfile)).size + if options.autoscale: + size = (getPanelViewResolution(sizeTmp, deviceres)) + else: size = (int(sizeTmp[0] * 1.5), int(sizeTmp[1] * 1.5)) - if size[0] <= deviceres[0]: + if size[0] - deviceres[0] < deviceres[0] * 0.01: noHorizontalPV = True else: noHorizontalPV = False - if size[1] <= deviceres[1]: + if size[1] - deviceres[1] < deviceres[1] * 0.01: noVerticalPV = True else: noVerticalPV = False @@ -193,7 +186,7 @@ def buildHTML(path, imgfile, imgfilepath): for box in boxes: f.writelines(["
\n", "\n", + imgfile, "\" width=\"" + str(size[0]) + "\" height=\"" + str(size[1]) + "\"/>\n", "
\n"]) f.writelines(["\n", "\n"]) @@ -336,29 +329,6 @@ def buildOPF(dstdir, title, filelist, cover=None): f.write("\n\n") else: f.write("\n\n") - # if options.iskindle and options.profile != 'Custom': - # if options.righttoleft: - # nextflow = 'right' - # else: - # nextflow = 'left' - # for entry in reflist: - # if '-kcc-b' in entry: - # if options.righttoleft: - # f.write("\n") - # else: - # f.write("\n") - # elif '-kcc-c' in entry: - # if options.righttoleft: - # f.write("\n") - # else: - # f.write("\n") - # else: - # f.write("\n") - # if nextflow == 'right': - # nextflow = 'left' - # else: - # nextflow = 'right' - # else: for entry in reflist: f.write("\n") f.write("\n\n") @@ -460,17 +430,15 @@ def buildEPUB(path, chapterNames, tomeNumber): chapter = False dirnames, filenames = walkSort(dirnames, filenames) for afile in filenames: - filename = getImageFileName(afile) - if not filename[0].endswith('-hq'): - filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile))) - if not chapter: - chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1])) - chapter = True - if cover is None: - cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'), - 'cover' + getImageFileName(filelist[-1][1])[1]) - options.covers.append((image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, - tomeNumber), options.uuid)) + filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile))) + if not chapter: + chapterlist.append((dirpath.replace('Images', 'Text'), filelist[-1][1])) + chapter = True + if cover is None: + cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'), + 'cover' + getImageFileName(filelist[-1][1])[1]) + options.covers.append((image.Cover(os.path.join(filelist[-1][0], filelist[-1][1]), cover, options, + tomeNumber), options.uuid)) # Overwrite chapternames if tree is flat and ComicInfo.xml has bookmarks if not chapterNames and options.chapters: chapterlist = [] @@ -548,10 +516,10 @@ def imgFileProcessing(work): workImg = image.ComicPageParser((dirpath, afile), opt) for i in workImg.payload: img = image.ComicPage(i[0], i[1], i[2], i[3], i[4], opt) - if opt.cropping > 0 and not opt.webtoon: - img.cropWhiteSpace(opt.croppingp) if opt.cropping == 2 and not opt.webtoon: - img.cutPageNumber(opt.croppingpn) + img.cropPageNumber(opt.croppingp) + if opt.cropping > 0 and not opt.webtoon: + img.cropMargin(opt.croppingp) img.autocontrastImage() img.resizeImage() if opt.forcepng and not opt.forcecolor: @@ -653,7 +621,7 @@ def getComicInfo(path, originalPath): try: xml = metadata.MetadataParser(xmlPath) except Exception: - os.remove(xmlPath) + saferRemove(xmlPath) return options.authors = [] if defaultTitle: @@ -678,7 +646,7 @@ def getComicInfo(path, originalPath): options.chapters = xml.data['Bookmarks'] if xml.data['Summary']: options.summary = escape(xml.data['Summary']) - os.remove(xmlPath) + saferRemove(xmlPath) def getCoversFromMCB(mangaID): @@ -704,6 +672,11 @@ def getDirectorySize(start_path='.'): return total_size +def getPanelViewResolution(imageSize, deviceRes): + scale = float(deviceRes[0]) / float(imageSize[0]) + return int(deviceRes[0]), int(scale * imageSize[1]) + + def getPanelViewSize(deviceres, size): x = int(deviceres[0] / 2 - size[0] / 2) / deviceres[0] * 100 y = int(deviceres[1] / 2 - size[1] / 2) / deviceres[1] * 100 @@ -963,6 +936,8 @@ def makeParser(): " KoA, KoAHD, KoAH2O, KoAO) [Default=KV]") mainOptions.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False, help="Manga style (right-to-left reading and splitting)") + mainOptions.add_option("-2", "--two-panel", action="store_true", dest="autoscale", default=False, + help="Display two not four panels in Panel View mode") mainOptions.add_option("-w", "--webtoon", action="store_true", dest="webtoon", default=False, help="Webtoon processing mode"), @@ -983,8 +958,10 @@ def makeParser(): help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]") processingOptions.add_option("-g", "--gamma", type="float", dest="gamma", default="0.0", help="Apply gamma correction to linearize the image [Default=Auto]") - processingOptions.add_option("--hq", action="store_true", dest="hqmode", default=False, - help="Enable high quality Panel View") + processingOptions.add_option("-c", "--cropping", type="int", dest="cropping", default="2", + help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]") + processingOptions.add_option("--cp", "--croppingpower", type="float", dest="croppingp", default="1.0", + help="Set cropping power [Default=1.0]") processingOptions.add_option("--blackborders", action="store_true", dest="black_borders", default=False, help="Disable autodetection and force black borders") processingOptions.add_option("--whiteborders", action="store_true", dest="white_borders", default=False, @@ -993,12 +970,6 @@ def makeParser(): help="Don't convert images to grayscale") processingOptions.add_option("--forcepng", action="store_true", dest="forcepng", default=False, help="Create PNG files instead JPEG") - processingOptions.add_option("--cropping", type="int", dest="cropping", default="2", - help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]") - processingOptions.add_option("--croppingpower", type="float", dest="croppingp", default="0.1", - help="Set margin cropping threshold [Default=0.1]") - processingOptions.add_option("--croppingpowerpage", type="float", dest="croppingpn", default="5.0", - help="Set page number cropping threshold [Default=5.0]") customProfileOptions.add_option("--customwidth", type="int", dest="customwidth", default=0, help="Replace screen width provided by device profile") @@ -1040,20 +1011,16 @@ def checkOptions(): # Older Kindle don't need higher resolution files due lack of Panel View. if options.profile == 'K1' or options.profile == 'K2' or options.profile == 'K3' or options.profile == 'KDX': options.panelview = False - options.hqmode = False # Webtoon mode mandatory options if options.webtoon: options.panelview = False - options.hqmode = False options.righttoleft = False options.upscale = True # Disable all Kindle features for other e-readers if options.profile == 'OTHER': options.panelview = False - options.hqmode = False if 'Ko' in options.profile: options.panelview = False - options.hqmode = False # CBZ files on Kindle DX/DXG support higher resolution if options.profile == 'KDX' and options.format == 'CBZ': options.customheight = 1200 @@ -1066,7 +1033,7 @@ def checkOptions(): if options.customheight != 0: Y = options.customheight newProfile = ("Custom", (int(X), int(Y)), image.ProfileData.Palette16, - image.ProfileData.Profiles[options.profile][3], (int(int(X) * 1.5), int(int(Y) * 1.5))) + image.ProfileData.Profiles[options.profile][3]) image.ProfileData.Profiles["Custom"] = newProfile options.profile = "Custom" options.profileData = image.ProfileData.Profiles[options.profile] @@ -1184,7 +1151,6 @@ def makeBook(source, qtGUI=None): if not GUI and options.format == 'MOBI': print("Creating MOBI files...") work = [] - k = kindle.Kindle() for i in filepath: work.append([i]) output = makeMOBI(work, GUI) @@ -1193,6 +1159,7 @@ def makeBook(source, qtGUI=None): print('Error: KindleGen failed to create MOBI!') print(errors) return filepath + k = kindle.Kindle() if k.path and k.coverSupport: print("Kindle detected. Uploading covers...") for i in filepath: @@ -1201,14 +1168,14 @@ def makeBook(source, qtGUI=None): print('Error: Failed to tweak KindleGen output!') return filepath else: - os.remove(i.replace('.epub', '.mobi') + '_toclean') + saferRemove(i.replace('.epub', '.mobi') + '_toclean') if k.path and k.coverSupport: options.covers[filepath.index(i)][0].saveToKindle(k, options.covers[filepath.index(i)][1]) return filepath def makeMOBIFix(item, uuid): - os.remove(item) + saferRemove(item) mobiPath = item.replace('.epub', '.mobi') move(mobiPath, mobiPath + '_toclean') try: diff --git a/kcc/comic2panel.py b/kcc/comic2panel.py index 6602a91c..def82509 100644 --- a/kcc/comic2panel.py +++ b/kcc/comic2panel.py @@ -24,7 +24,7 @@ from optparse import OptionParser, OptionGroup from multiprocessing import Pool from PIL import Image, ImageStat, ImageOps -from .shared import getImageFileName, walkLevel, walkSort +from .shared import getImageFileName, walkLevel, walkSort, saferRemove try: from PyQt5 import QtCore except ImportError: @@ -77,7 +77,7 @@ def mergeDirectory(work): img = ImageOps.fit(img, (targetWidth, img.size[1]), method=Image.BICUBIC, centering=(0.5, 0.5)) result.paste(img, (0, y)) y += img.size[1] - os.remove(i) + saferRemove(i) savePath = os.path.split(imagesValid[0]) result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG') except Exception: @@ -203,7 +203,7 @@ def splitImage(work): targetHeight += panels[panel][2] newPage.save(os.path.join(path, fileExpanded[0] + '-' + str(pageNumber) + '.png'), 'PNG') pageNumber += 1 - os.remove(filePath) + saferRemove(filePath) except Exception: return str(sys.exc_info()[1]) @@ -275,7 +275,7 @@ def main(argv=None, qtGUI=None): pagenumber += 1 work.append([root, name, options]) else: - os.remove(os.path.join(root, name)) + saferRemove(os.path.join(root, name)) if GUI: GUI.progressBarTick.emit('Splitting images') GUI.progressBarTick.emit(str(pagenumber)) diff --git a/kcc/image.py b/kcc/image.py index aef77e53..009b2bbf 100755 --- a/kcc/image.py +++ b/kcc/image.py @@ -1,5 +1,6 @@ # 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-2016 Pawel Jastrzebski # @@ -20,7 +21,7 @@ from io import BytesIO from urllib.request import Request, urlopen from urllib.parse import quote -from PIL import Image, ImageOps, ImageStat, ImageChops +from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter from .shared import md5Checksum from . import __version__ @@ -77,21 +78,21 @@ def __init__(self): ] Profiles = { - 'K1': ("Kindle 1", (600, 670), Palette4, 1.8, (900, 1005)), - 'K2': ("Kindle 2", (600, 670), Palette15, 1.8, (900, 1005)), - 'K3': ("Kindle", (600, 800), Palette16, 1.8, (900, 1200)), - 'K45': ("Kindle", (600, 800), Palette16, 1.8, (900, 1200)), - 'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8, (1236, 1500)), - 'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8, (1137, 1536)), - 'KV': ("Kindle Paperwhite 3/Voyage/Oasis", (1072, 1448), Palette16, 1.8, (1608, 2172)), - 'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8, (900, 1200)), - 'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8, (1152, 1536)), - 'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8, (1608, 2172)), - 'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8, (1137, 1536)), - 'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8, (1620, 2160)), - 'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8, (1620, 2145)), - 'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8, (2106, 2808)), - 'OTHER': ("Other", (0, 0), Palette16, 1.8, (0, 0)), + 'K1': ("Kindle 1", (600, 670), Palette4, 1.8), + 'K2': ("Kindle 2", (600, 670), Palette15, 1.8), + 'K3': ("Kindle", (600, 800), Palette16, 1.8), + 'K45': ("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/Voyage/Oasis", (1072, 1448), 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), + 'OTHER': ("Other", (0, 0), Palette16, 1.8), } @@ -105,8 +106,6 @@ def __init__(self, source, options): self.color = self.colorCheck() self.fill = self.fillCheck() self.splitCheck() - if self.opt.hqmode: - self.sizeCheck() def getImageHistogram(self, image): histogram = image.histogram() @@ -205,29 +204,16 @@ def fillCheck(self): else: return 'white' - def sizeCheck(self): - additionalPayload = [] - width, height = self.image.size - dstwidth, dstheight = self.size - for work in self.payload: - if width > dstwidth and height > dstheight: - additionalPayload.append([work[0] + '+', work[1], work[2].copy(), work[3], work[4]]) - self.payload = self.payload + additionalPayload - class ComicPage: def __init__(self, mode, path, image, color, fill, options): self.opt = options - _, self.size, self.palette, self.gamma, self.panelviewsize = self.opt.profileData + _, self.size, self.palette, self.gamma = self.opt.profileData self.image = image self.color = color self.fill = fill self.rotated = False self.orgPath = os.path.join(path[0], path[1]) - if '+' in mode: - self.hqMode = True - else: - self.hqMode = False if 'N' in mode: self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC' elif 'R' in mode: @@ -247,8 +233,6 @@ def saveToDir(self): flags.append('Rotated') if self.fill != 'white': flags.append('BlackFill') - if self.hqMode: - self.targetPath += '-HQ' if self.opt.forcepng: self.targetPath += '.png' self.image.save(self.targetPath, 'PNG', optimize=1) @@ -282,128 +266,69 @@ def quantizeImage(self): self.image = self.image.quantize(palette=palImg) def resizeImage(self): - if self.hqMode: - size = (self.panelviewsize[0], self.panelviewsize[1]) - if self.image.size[0] > size[0] or self.image.size[1] > size[1]: - self.image.thumbnail(size, Image.LANCZOS) + if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]: + method = Image.BICUBIC else: - size = (self.size[0], self.size[1]) - if self.image.size[0] <= size[0] and self.image.size[1] <= size[1]: - method = Image.BICUBIC - else: - method = Image.LANCZOS - if self.opt.stretch: - self.image = self.image.resize(size, method) - elif self.image.size[0] <= size[0] and self.image.size[1] <= size[1] and not self.opt.upscale: - if self.opt.format == 'CBZ': - borderw = int((size[0] - self.image.size[0]) / 2) - borderh = int((size[1] - self.image.size[1]) / 2) - self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill) - if self.image.size[0] != size[0] or self.image.size[1] != size[1]: - self.image = ImageOps.fit(self.image, size, method=Image.BICUBIC, centering=(0.5, 0.5)) + method = Image.LANCZOS + if self.opt.stretch: + 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': + 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': + 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: - if self.opt.format == 'CBZ': - ratioDev = float(size[0]) / float(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, size, method=method, centering=(0.5, 0.5)) - else: - hpercent = size[1] / float(self.image.size[1]) - wsize = int((float(self.image.size[0]) * float(hpercent))) - self.image = self.image.resize((wsize, size[1]), method) - if self.image.size[0] > size[0] or self.image.size[1] > size[1]: - self.image.thumbnail(size, Image.LANCZOS) + 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 cutPageNumber(self, fixedThreshold): - if ImageChops.invert(self.image).getbbox() is not None: - widthImg, heightImg = self.image.size - delta = 2 - diff = delta - if ImageStat.Stat(self.image).var[0] < 2 * fixedThreshold: - return self.image - while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < fixedThreshold\ - and diff < heightImg: - diff += delta - diff -= delta - pageNumberCut1 = diff - if diff < delta: - diff = delta - oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - diff += delta - while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - oldStat > 0\ - and diff < heightImg // 4: - oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] - diff += delta - diff -= delta - pageNumberCut2 = diff - diff += delta - oldStat = ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, - heightImg - pageNumberCut2))).var[0] - while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg - pageNumberCut2))).var[0]\ - < fixedThreshold + oldStat and diff < heightImg // 4: - diff += delta - diff -= delta - pageNumberCut3 = diff - delta = 5 - diff = delta - while ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut2, diff, heightImg))).var[0]\ - < fixedThreshold and diff < widthImg: - diff += delta - diff -= delta - pageNumberX1 = diff - diff = delta - while ImageStat.Stat(self.image.crop((widthImg - diff, heightImg - pageNumberCut2, - widthImg, heightImg))).var[0] < fixedThreshold and diff < widthImg: - diff += delta - diff -= delta - pageNumberX2 = widthImg - diff - if pageNumberCut3 - pageNumberCut1 > 2 * delta\ - and float(pageNumberX2 - pageNumberX1) / float(pageNumberCut2 - pageNumberCut1) <= 9.0\ - and ImageStat.Stat(self.image.crop((0, heightImg - pageNumberCut3, widthImg, heightImg))).var[0]\ - / ImageStat.Stat(self.image).var[0] < 0.1\ - and pageNumberCut3 < heightImg / 4 - delta: - diff = pageNumberCut3 - else: - diff = pageNumberCut1 - self.image = self.image.crop((0, 0, widthImg, heightImg - diff)) + def getBoundingBox(self, tmpImg): + min_margin = [int(0.005 * i + 0.5) for i in tmpImg.size] + max_margin = [int(0.1 * i + 0.5) for i in tmpImg.size] + bbox = tmpImg.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(tmpImg.size[0], + max(tmpImg.size[0] - max_margin[0], bbox[2] + min_margin[0])), + min(tmpImg.size[1], + max(tmpImg.size[1] - max_margin[1], bbox[3] + min_margin[1])), + ) + return bbox - def cropWhiteSpace(self, fixedThreshold): - if ImageChops.invert(self.image).getbbox() is not None: - widthImg, heightImg = self.image.size - delta = 10 - diff = delta - # top - while ImageStat.Stat(self.image.crop((0, 0, widthImg, diff))).var[0] < fixedThreshold and diff < heightImg: - diff += delta - diff -= delta - self.image = self.image.crop((0, diff, widthImg, heightImg)) - widthImg, heightImg = self.image.size - diff = delta - # left - while ImageStat.Stat(self.image.crop((0, 0, diff, heightImg))).var[0] < fixedThreshold and diff < widthImg: - diff += delta - diff -= delta - self.image = self.image.crop((diff, 0, widthImg, heightImg)) - widthImg, heightImg = self.image.size - diff = delta - # down - while ImageStat.Stat(self.image.crop((0, heightImg - diff, widthImg, heightImg))).var[0] < fixedThreshold\ - and diff < heightImg: - diff += delta - diff -= delta - self.image = self.image.crop((0, 0, widthImg, heightImg - diff)) - widthImg, heightImg = self.image.size - diff = delta - # right - while ImageStat.Stat(self.image.crop((widthImg - diff, 0, widthImg, heightImg))).var[0] < fixedThreshold\ - and diff < widthImg: - diff += delta - diff -= delta - self.image = self.image.crop((0, 0, widthImg - diff, heightImg)) + def cropPageNumber(self, power): + if self.fill != 'white': + tmpImg = self.image.convert(mode='L') + else: + tmpImg = ImageOps.invert(self.image.convert(mode='L')) + tmpImg = tmpImg.point(lambda x: x and 255) + tmpImg = tmpImg.filter(ImageFilter.MinFilter(size=3)) + tmpImg = tmpImg.filter(ImageFilter.GaussianBlur(radius=5)) + tmpImg = tmpImg.point(lambda x: (x >= 48 * power) and x) + self.image = self.image.crop(tmpImg.getbbox()) if tmpImg.getbbox() else self.image + + def cropMargin(self, power): + if self.fill != 'white': + tmpImg = self.image.convert(mode='L') + else: + tmpImg = ImageOps.invert(self.image.convert(mode='L')) + tmpImg = tmpImg.filter(ImageFilter.GaussianBlur(radius=3)) + tmpImg = tmpImg.point(lambda x: (x >= 16 * power) and x) + self.image = self.image.crop(self.getBoundingBox(tmpImg)) if tmpImg.getbbox() else self.image class Cover: diff --git a/kcc/shared.py b/kcc/shared.py index c1495024..88d1782d 100644 --- a/kcc/shared.py +++ b/kcc/shared.py @@ -144,8 +144,7 @@ def removeFromZIP(zipfname, *filenames): def sanitizeTrace(traceback): return ''.join(format_tb(traceback))\ - .replace('C:\\Users\\pawel\\Documents\\Projekty\\KCC\\', '') \ - .replace('C:\\Users\\Paweł\\Documents\\Projekty\\KCC\\', '') \ + .replace('C:\\Users\\Pawel\\Documents\\Projekty\\KCC\\', '') \ .replace('C:\\Python35\\', '')\ .replace('c:\\python35\\', '') diff --git a/other/osx/Info.plist b/other/osx/Info.plist index 0194378c..d883b4d2 100644 --- a/other/osx/Info.plist +++ b/other/osx/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable MacOS/Kindle Comic Converter CFBundleGetInfoString - KindleComicConverter 5.1.3, written 2012-2016 by Ciro Mattia Gonano and Pawel Jastrzebski + KindleComicConverter 5.2, written 2012-2016 by Ciro Mattia Gonano and Pawel Jastrzebski CFBundleIconFile comic2ebook.icns CFBundleIdentifier @@ -21,11 +21,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 5.1.3 + 5.2.0 CFBundleSignature ???? CFBundleVersion - 5.1.3 + 5.2.0 LSEnvironment PATH