1
0
mirror of https://github.com/ciromattia/kcc synced 2025-12-13 01:36:27 +00:00

Add PDF output support (#1032)

* Add PDF output support

* optimize

* use with statement

* OS_SORT_KEY

* fix import

* simplify

* fix None

* fix conditional

---------

Co-authored-by: Belgian Coder <>
Co-authored-by: Alex Xu <alexkurosakimh3@gmail.com>
This commit is contained in:
Belgian Coder
2025-08-11 19:36:33 +02:00
committed by GitHub
parent 6a7500441d
commit 704dcd6dbe
5 changed files with 76 additions and 16 deletions

View File

@@ -12,7 +12,11 @@ like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins, Pages display in fullscreen without margins,
with proper fixed layout support. with proper fixed layout support.
Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs. Supported input formats include JPG/PNG/GIF image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, and CBZ. Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical, The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
which can be directly converted by KCC in v9+. Be sure to check the spread shift option! which can be directly converted by KCC in v9+. Be sure to check the spread shift option!
@@ -256,7 +260,7 @@ OUTPUT SETTINGS:
-a AUTHOR, --author AUTHOR -a AUTHOR, --author AUTHOR
Author name [Default=KCC] Author name [Default=KCC]
-f FORMAT, --format FORMAT -f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto] Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub' --nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
-b BATCHSPLIT, --batchsplit BATCHSPLIT -b BATCHSPLIT, --batchsplit BATCHSPLIT
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0] Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]

View File

@@ -324,6 +324,9 @@ class WorkerThread(QThread):
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False) MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files' GUI.progress.content = 'Creating CBZ files'
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files', 'info', False)
GUI.progress.content = 'Creating PDF files'
else: else:
MW.addMessage.emit('Creating EPUB files', 'info', False) MW.addMessage.emit('Creating EPUB files', 'info', False)
GUI.progress.content = 'Creating EPUB files' GUI.progress.content = 'Creating EPUB files'
@@ -368,6 +371,8 @@ class WorkerThread(QThread):
GUI.progress.content = '' GUI.progress.content = ''
if gui_current_format == 'CBZ': if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating CBZ files... <b>Done!</b>', 'info', True)
elif gui_current_format == 'PDF':
MW.addMessage.emit('Creating PDF files... <b>Done!</b>', 'info', True)
else: else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True) MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format: if 'MOBI' in gui_current_format:
@@ -1011,6 +1016,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'}, "MOBI/AZW3": {'icon': 'MOBI', 'format': 'MOBI'},
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'}, "EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'}, "CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'}, "KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'}, "MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'}, "EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
@@ -1092,11 +1098,11 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'Label': 'KoS'}, 'Label': 'KoS'},
"Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "Kobo Elipsa": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'KoE'}, 'Label': 'KoE'},
"reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 1": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk1'}, 'Label': 'Rmk1'},
"reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': False, "reMarkable 2": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': False,
'Label': 'Rmk2'}, 'Label': 'Rmk2'},
"reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 1, 'DefaultUpscale': True, 'ForceColor': True, "reMarkable Paper Pro": {'PVOptions': False, 'ForceExpert': False, 'DefaultFormat': 3, 'DefaultUpscale': True, 'ForceColor': True,
'Label': 'RmkPP'}, 'Label': 'RmkPP'},
"Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False, "Other": {'PVOptions': False, 'ForceExpert': True, 'DefaultFormat': 1, 'DefaultUpscale': False, 'ForceColor': False,
'Label': 'OTHER'}, 'Label': 'OTHER'},

View File

@@ -582,6 +582,34 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, len
buildOPF(path, options.title, filelist, cover) buildOPF(path, options.title, filelist, cover)
def buildPDF(path, title, cover=None, output_file=None):
"""
Build a PDF file from processed comic images.
Images are combined into a single PDF optimized for e-readers.
"""
start = perf_counter()
# open empty PDF
with pymupdf.open() as doc:
# Stream images to PDF
for root, dirs, files in os.walk(os.path.join(path, "OEBPS", "Images")):
files.sort(key=OS_SORT_KEY)
dirs.sort(key=OS_SORT_KEY)
for file in files:
w, h = Image.open(os.path.join(root, file)).size
page = doc.new_page(width=w, height=h)
page.insert_image(page.rect, filename=os.path.join(root, file))
# determine output filename if not provided
if output_file is None:
output_file = getOutputFilename(path, None, '.pdf', '')
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
print(f"MuPDF output: {end-start} sec")
return output_file
def imgDirectoryProcessing(path): def imgDirectoryProcessing(path):
global workerPool, workerOutput global workerPool, workerOutput
workerPool = Pool(maxtasksperchild=100) workerPool = Pool(maxtasksperchild=100)
@@ -659,6 +687,7 @@ def imgFileProcessing(work):
pass pass
elif opt.forcepng: elif opt.forcepng:
img.convertToGrayscale() img.convertToGrayscale()
if opt.format != 'PDF':
img.quantizeImage() img.quantizeImage()
else: else:
img.convertToGrayscale() img.convertToGrayscale()
@@ -1137,6 +1166,7 @@ def detectSuboptimalProcessing(tmppath, orgpath):
try: try:
img = Image.open(path) img = Image.open(path)
imageNumber += 1 imageNumber += 1
# count images smaller than device resolution
if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]: if options.profileData[1][0] > img.size[0] and options.profileData[1][1] > img.size[1]:
imageSmaller += 1 imageSmaller += 1
except Exception as err: except Exception as err:
@@ -1184,7 +1214,6 @@ def slugify(value, is_natural_sorted):
value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2)) value = sub(r'0*([0-9]{4,})', r'\1', sub(r'([0-9]+)', r'0000\1', value, count=2))
return value return value
def makeZIP(zipfilename, basedir, isepub=False): def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter() start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip' zipfilename = os.path.abspath(zipfilename) + '.zip'
@@ -1209,7 +1238,6 @@ def makeZIP(zipfilename, basedir, isepub=False):
print(f"makeZIP time: {end - start} seconds") print(f"makeZIP time: {end - start} seconds")
return zipfilename return zipfilename
def makeParser(): def makeParser():
psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False) psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
@@ -1247,7 +1275,7 @@ def makeParser():
output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor", output_options.add_argument("-a", "--author", action="store", dest="author", default="defaultauthor",
help="Author name [Default=KCC]") help="Author name [Default=KCC]")
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto", output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) " help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB, PDF) "
"[Default=Auto]") "[Default=Auto]")
output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False, output_options.add_argument("--nokepub", action="store_true", dest="noKepub", default=False,
help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'") help="If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'")
@@ -1337,6 +1365,8 @@ def checkOptions(options):
options.format = 'CBZ' options.format = 'CBZ'
elif options.profile in image.ProfileData.ProfilesKindle.keys(): elif options.profile in image.ProfileData.ProfilesKindle.keys():
options.format = 'MOBI' options.format = 'MOBI'
elif options.profile in image.ProfileData.ProfilesRemarkable.keys():
options.format = 'PDF'
else: else:
options.format = 'EPUB' options.format = 'EPUB'
if options.profile in image.ProfileData.ProfilesKindle.keys(): if options.profile in image.ProfileData.ProfilesKindle.keys():
@@ -1500,6 +1530,8 @@ def makeBook(source, qtgui=None):
if GUI: if GUI:
if options.format == 'CBZ': if options.format == 'CBZ':
GUI.progressBarTick.emit('Compressing CBZ files') GUI.progressBarTick.emit('Compressing CBZ files')
elif options.format == 'PDF':
GUI.progressBarTick.emit('Creating PDF files')
else: else:
GUI.progressBarTick.emit('Compressing EPUB files') GUI.progressBarTick.emit('Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1)) GUI.progressBarTick.emit(str(len(tomes) + 1))
@@ -1521,6 +1553,14 @@ def makeBook(source, qtgui=None):
else: else:
filepath.append(getOutputFilename(source, options.output, '.cbz', '')) filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images")) makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
elif options.format == 'PDF':
print("Creating PDF file with PyMuPDF...")
# determine output filename based on source and tome count
suffix = (' ' + str(tomeNumber)) if len(tomes) > 1 else ''
output_file = getOutputFilename(source, options.output, '.pdf', suffix)
# use optimized buildPDF logic with streaming and compression
output_pdf = buildPDF(tome, options.title, None, output_file)
filepath.append(output_pdf)
else: else:
print("Creating EPUB file...") print("Creating EPUB file...")
if len(tomes) > 1: if len(tomes) > 1:
@@ -1530,6 +1570,8 @@ def makeBook(source, qtgui=None):
buildEPUB(tome, chapterNames, tomeNumber, False, cover) buildEPUB(tome, chapterNames, tomeNumber, False, cover)
filepath.append(getOutputFilename(source, options.output, '.epub', '')) filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, True) makeZIP(tome + '_comic', tome, True)
# Copy files to final destination (PDF files are already saved directly)
if options.format != 'PDF':
copyfile(tome + '_comic.zip', filepath[-1]) copyfile(tome + '_comic.zip', filepath[-1])
try: try:
os.remove(tome + '_comic.zip') os.remove(tome + '_comic.zip')
@@ -1570,6 +1612,12 @@ def makeBook(source, qtgui=None):
end = perf_counter() end = perf_counter()
print(f"makeBook: {end - start} seconds") print(f"makeBook: {end - start} seconds")
# Clean up temporary workspace
try:
rmtree(path, True)
except Exception:
pass
print(filepath)
return filepath return filepath
@@ -1649,3 +1697,4 @@ def makeMOBI(work, qtgui=None):
makeMOBIWorkerPool.close() makeMOBIWorkerPool.close()
makeMOBIWorkerPool.join() makeMOBIWorkerPool.join()
return makeMOBIWorkerOutput return makeMOBIWorkerOutput

View File

@@ -9,4 +9,4 @@ mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0 natsort>=8.4.0
distro>=1.8.0 distro>=1.8.0
numpy>=1.22.4 numpy>=1.22.4
PyMuPDF>=1.26.1 PyMuPDF>=1.18.0

View File

@@ -79,6 +79,7 @@ setuptools.setup(
install_requires=[ install_requires=[
'pyside6>=6.5.1', 'pyside6>=6.5.1',
'Pillow>=11.3.0', 'Pillow>=11.3.0',
'PyMuPDF>=1.18.0',
'psutil>=5.9.5', 'psutil>=5.9.5',
'python-slugify>=1.2.1,<9.0.0', 'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0', 'raven>=6.0.0',