mirror of
https://github.com/ciromattia/kcc
synced 2026-04-16 05:58:52 +00:00
Compare commits
28 Commits
v7.4.1
...
clone-fork
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9ee43278 | ||
|
|
c4f845c221 | ||
|
|
ebb59dbc2d | ||
|
|
8da2b4cb96 | ||
|
|
7b8858678f | ||
|
|
be07e0df6a | ||
|
|
dc711e671d | ||
|
|
581ecd0ec2 | ||
|
|
f3a32c6174 | ||
|
|
0ce5f7f186 | ||
|
|
c3d2f89471 | ||
|
|
b1c4cd36f1 | ||
|
|
b7c6281b55 | ||
|
|
559485184d | ||
|
|
75f5274449 | ||
|
|
dfc149d893 | ||
|
|
327b522080 | ||
|
|
7c029b4ba1 | ||
|
|
2db8f5c8fd | ||
|
|
ce72921289 | ||
|
|
f1bbc47798 | ||
|
|
5a39007db6 | ||
|
|
0d7487f8d4 | ||
|
|
8a78f82ff2 | ||
|
|
149f7e5921 | ||
|
|
40d219de4d | ||
|
|
370d1d7392 | ||
|
|
8295f163c2 |
1
.github/workflows/package-linux.yml
vendored
1
.github/workflows/package-linux.yml
vendored
@@ -70,6 +70,5 @@ jobs:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
*.AppImage*
|
||||
|
||||
1
.github/workflows/package-macos.yml
vendored
1
.github/workflows/package-macos.yml
vendored
@@ -89,7 +89,6 @@ jobs:
|
||||
prerelease: true
|
||||
generate_release_notes: true
|
||||
files: |
|
||||
CHANGELOG.md
|
||||
LICENSE.txt
|
||||
dist/*.dmg
|
||||
- name: Clean up keychain and provisioning profile
|
||||
|
||||
@@ -10,46 +10,37 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
entry: [ kcc-c2e, kcc-c2p ]
|
||||
include:
|
||||
- entry: kcc-c2e
|
||||
capital: KCC_c2e
|
||||
- entry: kcc-c2p
|
||||
capital: KCC_c2p
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@v4
|
||||
# with:
|
||||
# python-version: 3.11
|
||||
# cache: 'pip'
|
||||
# - name: Install python dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip setuptools wheel pyinstaller
|
||||
# pip install -r requirements.txt
|
||||
# - name: build binary
|
||||
# run: |
|
||||
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
|
||||
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
path: .
|
||||
spec: ./kcc-c2e.spec
|
||||
- name: Package Application
|
||||
uses: JackMcKew/pyinstaller-action-windows@main
|
||||
with:
|
||||
path: .
|
||||
spec: ./kcc-c2p.spec
|
||||
spec: ./${{ matrix.entry }}.spec
|
||||
- name: rename binaries
|
||||
run: |
|
||||
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
|
||||
mv dist/windows/kcc-c2e.exe dist/windows/KCC_c2e_${version_built}.exe
|
||||
mv dist/windows/kcc-c2p.exe dist/windows/KCC_c2p_${version_built}.exe
|
||||
mv dist/windows/${{ matrix.entry }}.exe dist/windows/${{ matrix.capital }}_${version_built}.exe
|
||||
|
||||
- name: upload-unsigned-artifact
|
||||
id: upload-unsigned-artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-build
|
||||
name: windows-build-${{ matrix.entry }}
|
||||
path: dist/windows/*.exe
|
||||
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v1.1
|
||||
uses: signpath/github-action-submit-signing-request@v1.2
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
|
||||
2
.github/workflows/package-windows.yml
vendored
2
.github/workflows/package-windows.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
name: windows-build
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v1.1
|
||||
uses: signpath/github-action-submit-signing-request@v1.2
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
|
||||
19
README.md
19
README.md
@@ -1,3 +1,5 @@
|
||||
<img src="header.jpg" alt="Header Image" width="400">
|
||||
|
||||
# KCC
|
||||
|
||||
|
||||
@@ -6,12 +8,13 @@
|
||||
[](https://github.com/ciromattia/kcc/pkgs/container/kcc)
|
||||
|
||||
**Kindle Comic Converter** optimizes comics and manga for eink readers like Kindle, Kobo, ReMarkable, and more.
|
||||
Pages display in fullscreen without margins, with proper fixed layout support.
|
||||
Its main feature is various optional image processing steps to look good on eink screens,
|
||||
which have different requirements than normal LCD screens.
|
||||
It also does filesize optimization by downscaling to your specific device's screen resolution,
|
||||
which can improve performance on underpowered ereaders.
|
||||
Supported input formats include folders/CBZ/CBR/PDF of JPG/PNG files and more.
|
||||
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ.
|
||||
Supported input formats include folders and archives of JPG/PNG files and more.
|
||||
Supported output formats include virtual panel view MOBI/AZW3, EPUB, KEPUB, and CBZ.
|
||||
|
||||

|
||||
|
||||
@@ -74,6 +77,8 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
|
||||
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
|
||||
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
|
||||
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
|
||||
- Huge margins / slow page turns?
|
||||
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
|
||||
|
||||
## PREREQUISITES
|
||||
|
||||
@@ -245,13 +250,13 @@ OTHER:
|
||||
|
||||
This section is for developers who want to contribute to KCC or power users who want to run the latest code without waiting for an official release.
|
||||
|
||||
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
|
||||
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone your fork of the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
|
||||
|
||||
Depending on your system [Python](https://www.python.org) may be called either `python` or `python3`. We use virtual environments (venv) to manage dependencies.
|
||||
|
||||
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
|
||||
|
||||
If you want to edit the `.ui` files, use [Qt Creator](https://www.qt.io/download-qt-installer-oss), included in **Qt for desktop development**.
|
||||
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
|
||||
Then use the `gen_ui_files` scripts to autogenerate the python UI.
|
||||
|
||||
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
|
||||
@@ -323,6 +328,12 @@ The app relies and includes the following scripts:
|
||||
- Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License.
|
||||
|
||||
## SAMPLE FILES CREATED BY KCC
|
||||
|
||||
https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples
|
||||
|
||||
Older links (dead):
|
||||
|
||||
|
||||
* [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi)
|
||||
* [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
|
||||
* [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)
|
||||
|
||||
BIN
header.jpg
Normal file
BIN
header.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 921 KiB |
@@ -424,6 +424,8 @@ class WorkerThread(QThread):
|
||||
MW.addMessage.emit('Created EPUB file was too big.', 'error', False)
|
||||
MW.addMessage.emit('EPUB file: ' + str(epubSize) + 'MB. Supported size: ~350MB.', 'error',
|
||||
False)
|
||||
if self.kindlegenErrorCode[0] == 3221226505:
|
||||
MW.addMessage.emit('Unknown Windows error. Possibly filepath too long?', 'error', False)
|
||||
else:
|
||||
for item in outputPath:
|
||||
if GUI.targetDirectory and GUI.targetDirectory != os.path.dirname(item):
|
||||
@@ -878,7 +880,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
|
||||
versionCheck = subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
|
||||
self.kindleGen = True
|
||||
for line in versionCheck.stdout.splitlines():
|
||||
if 'Amazon kindlegen' in line:
|
||||
|
||||
@@ -36,7 +36,7 @@ from uuid import uuid4
|
||||
from natsort import os_sort_keygen
|
||||
from slugify import slugify as slugify_ext
|
||||
from PIL import Image, ImageFile
|
||||
from subprocess import STDOUT, PIPE
|
||||
from subprocess import STDOUT, PIPE, CalledProcessError
|
||||
from psutil import virtual_memory, disk_usage
|
||||
from html import escape as hescape
|
||||
|
||||
@@ -285,9 +285,9 @@ def buildOPF(dstdir, title, filelist, cover=None):
|
||||
"<dc:identifier id=\"BookID\">urn:uuid:", options.uuid, "</dc:identifier>\n",
|
||||
"<dc:contributor id=\"contributor\">KindleComicConverter-" + __version__ + "</dc:contributor>\n"])
|
||||
if len(options.summary) > 0:
|
||||
f.writelines(["<dc:description>", options.summary, "</dc:description>\n"])
|
||||
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
|
||||
for author in options.authors:
|
||||
f.writelines(["<dc:creator>", author, "</dc:creator>\n"])
|
||||
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
|
||||
f.writelines(["<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n",
|
||||
"<meta name=\"cover\" content=\"cover\"/>\n"])
|
||||
if options.iskindle and options.profile != 'Custom':
|
||||
@@ -513,8 +513,11 @@ def buildEPUB(path, chapternames, tomenumber, ischunked):
|
||||
dirnames, filenames = walkSort(dirnames, filenames)
|
||||
for afile in filenames:
|
||||
if cover is None:
|
||||
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
|
||||
'cover' + getImageFileName(afile)[1])
|
||||
try:
|
||||
cover = os.path.join(os.path.join(path, 'OEBPS', 'Images'),
|
||||
'cover' + getImageFileName(afile)[1])
|
||||
except Exception as e:
|
||||
raise UserWarning(f"{afile}: {e}")
|
||||
options.covers.append((image.Cover(os.path.join(dirpath, afile), cover, options,
|
||||
tomenumber), options.uuid))
|
||||
if not chapter:
|
||||
@@ -579,6 +582,10 @@ def imgDirectoryProcessing(path):
|
||||
workerPool.join()
|
||||
img_processing_end = perf_counter()
|
||||
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
|
||||
|
||||
# macOS 15 likes to add ._ files after multiprocessing
|
||||
dot_clean(path)
|
||||
|
||||
if GUI and not GUI.conversionAlive:
|
||||
rmtree(os.path.join(path, '..', '..'), True)
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
@@ -743,7 +750,7 @@ def getComicInfo(path, originalpath):
|
||||
return
|
||||
if defaultTitle:
|
||||
if xml.data['Series']:
|
||||
options.title = hescape(xml.data['Series'])
|
||||
options.title = xml.data['Series']
|
||||
if xml.data['Volume']:
|
||||
titleSuffix += ' V' + xml.data['Volume'].zfill(2)
|
||||
if xml.data['Number']:
|
||||
@@ -753,7 +760,7 @@ def getComicInfo(path, originalpath):
|
||||
options.authors = []
|
||||
for field in ['Writers', 'Pencillers', 'Inkers', 'Colorists']:
|
||||
for person in xml.data[field]:
|
||||
options.authors.append(hescape(person))
|
||||
options.authors.append(person)
|
||||
if len(options.authors) > 0:
|
||||
options.authors = list(set(options.authors))
|
||||
options.authors.sort()
|
||||
@@ -762,7 +769,7 @@ def getComicInfo(path, originalpath):
|
||||
if xml.data['Bookmarks']:
|
||||
options.comicinfo_chapters = xml.data['Bookmarks']
|
||||
if xml.data['Summary']:
|
||||
options.summary = hescape(xml.data['Summary'])
|
||||
options.summary = xml.data['Summary']
|
||||
os.remove(xmlPath)
|
||||
|
||||
|
||||
@@ -836,12 +843,25 @@ 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):
|
||||
for name in files:
|
||||
if name.startswith('._'):
|
||||
os.remove(os.path.join(root, name))
|
||||
|
||||
|
||||
def chunk_directory(path):
|
||||
level = -1
|
||||
for root, _, files in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
for f in files:
|
||||
# Windows MAX_LENGTH = 260 plus some buffer
|
||||
if len(os.path.join(root, f)) > 180:
|
||||
flattenTree(os.path.join(path, 'OEBPS', 'Images'))
|
||||
level = 1
|
||||
break
|
||||
if getImageFileName(f):
|
||||
newLevel = os.path.join(root, f).replace(os.path.join(path, 'OEBPS', 'Images'), '').count(os.sep)
|
||||
if level != -1 and level != newLevel:
|
||||
@@ -935,7 +955,8 @@ def detectSuboptimalProcessing(tmppath, orgpath):
|
||||
raise RuntimeError('Image file %s is corrupted. Error: %s' % (pathOrg, str(err)))
|
||||
else:
|
||||
try:
|
||||
os.remove(os.path.join(root, name))
|
||||
if os.path.exists(os.path.join(root, name)):
|
||||
os.remove(os.path.join(root, name))
|
||||
except OSError as e:
|
||||
raise RuntimeError(f"{name}: {e}")
|
||||
# remove empty nested folders
|
||||
@@ -1182,7 +1203,7 @@ def checkTools(source):
|
||||
sys.exit(1)
|
||||
if options.format == 'MOBI':
|
||||
try:
|
||||
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT)
|
||||
subprocess_run(['kindlegen', '-locale', 'en'], stdout=PIPE, stderr=STDOUT, check=True)
|
||||
except FileNotFoundError:
|
||||
print('ERROR: KindleGen is missing!')
|
||||
sys.exit(1)
|
||||
@@ -1345,27 +1366,28 @@ def makeMOBIWorker(item):
|
||||
try:
|
||||
if os.path.getsize(item) < 629145600:
|
||||
output = subprocess_run(['kindlegen', '-dont_append_source', '-locale', 'en', item],
|
||||
stdout=PIPE, stderr=STDOUT, encoding='UTF-8')
|
||||
for line in output.stdout.splitlines():
|
||||
# ERROR: Generic error
|
||||
if "Error(" in line:
|
||||
kindlegenErrorCode = 1
|
||||
kindlegenError = line
|
||||
# ERROR: EPUB too big
|
||||
if ":E23026:" in line:
|
||||
kindlegenErrorCode = 23026
|
||||
if kindlegenErrorCode > 0:
|
||||
break
|
||||
if ":I1036: Mobi file built successfully" in line:
|
||||
break
|
||||
stdout=PIPE, stderr=STDOUT, encoding='UTF-8', errors='ignore', check=True)
|
||||
else:
|
||||
# ERROR: EPUB too big
|
||||
kindlegenErrorCode = 23026
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
except Exception as err:
|
||||
except CalledProcessError as err:
|
||||
for line in err.stdout.splitlines():
|
||||
# ERROR: Generic error
|
||||
if "Error(" in line:
|
||||
kindlegenErrorCode = 1
|
||||
kindlegenError = line
|
||||
# ERROR: EPUB too big
|
||||
if ":E23026:" in line:
|
||||
kindlegenErrorCode = 23026
|
||||
if kindlegenErrorCode > 0:
|
||||
break
|
||||
if ":I1036: Mobi file built successfully" in line:
|
||||
break
|
||||
# ERROR: KCC unknown generic error
|
||||
kindlegenErrorCode = 1
|
||||
kindlegenError = format(err)
|
||||
if kindlegenErrorCode == 0:
|
||||
kindlegenErrorCode = err.returncode
|
||||
kindlegenError = err.stdout
|
||||
return [kindlegenErrorCode, kindlegenError, item]
|
||||
|
||||
|
||||
|
||||
@@ -368,16 +368,11 @@ class ComicPage:
|
||||
if self.opt.stretch:
|
||||
self.image = self.image.resize(self.size, method)
|
||||
elif method == Image.Resampling.BICUBIC 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=method)
|
||||
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:
|
||||
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:
|
||||
if self.kindle_scribe_azw3:
|
||||
@@ -385,7 +380,7 @@ class ComicPage:
|
||||
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]:
|
||||
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
|
||||
|
||||
@@ -20,6 +20,7 @@ import os
|
||||
from xml.dom.minidom import parse, Document
|
||||
from tempfile import mkdtemp
|
||||
from shutil import rmtree
|
||||
from xml.sax.saxutils import unescape
|
||||
from . import comicarchive
|
||||
|
||||
|
||||
@@ -52,19 +53,19 @@ class MetadataParser:
|
||||
|
||||
def parseXML(self):
|
||||
if len(self.rawdata.getElementsByTagName('Series')) != 0:
|
||||
self.data['Series'] = self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue
|
||||
self.data['Series'] = unescape(self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue)
|
||||
if len(self.rawdata.getElementsByTagName('Volume')) != 0:
|
||||
self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue
|
||||
if len(self.rawdata.getElementsByTagName('Number')) != 0:
|
||||
self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue
|
||||
if len(self.rawdata.getElementsByTagName('Summary')) != 0:
|
||||
self.data['Summary'] = self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue
|
||||
self.data['Summary'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue)
|
||||
if len(self.rawdata.getElementsByTagName('Title')) != 0:
|
||||
self.data['Title'] = self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue
|
||||
self.data['Title'] = unescape(self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue)
|
||||
for field in ['Writer', 'Penciller', 'Inker', 'Colorist']:
|
||||
if len(self.rawdata.getElementsByTagName(field)) != 0:
|
||||
for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '):
|
||||
self.data[field + 's'].append(person)
|
||||
self.data[field + 's'].append(unescape(person))
|
||||
self.data[field + 's'] = list(set(self.data[field + 's']))
|
||||
self.data[field + 's'].sort()
|
||||
if len(self.rawdata.getElementsByTagName('Page')) != 0:
|
||||
|
||||
Reference in New Issue
Block a user