1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-16 14:08:45 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Alex Xu
4b4860b976 Bump to 9.4.3 2026-01-26 14:45:28 -08:00
Alex Xu
56e8e24176 fix release notes (#1231) 2026-01-26 14:44:36 -08:00
Alex Xu
b0f8f1c633 fix file fusion bugs (#1230) 2026-01-26 14:39:05 -08:00
Alex Xu
38acc3bf05 skip blanks on image based pdfs (#1228) 2026-01-26 09:56:44 -08:00
Alex Xu
fbd5980b9b add Kindle 1240x1860 profile (#1227) 2026-01-26 09:41:49 -08:00
Alex Xu
667d702b8a Kindle Scribe 2025 warning 2026-01-25 13:45:15 -08:00
Alex Xu
9a4143ce62 Add legacy pdf image extract option (#1225) 2026-01-25 13:41:43 -08:00
Alex Xu
f63387cae4 remove corrupt image checking (#1221)
* remove corrupt image checking

Removed image verification step before copying the image.

* Update image.py
2026-01-20 20:48:18 -08:00
Alex Xu
f5fd2bb7fe fix cropping divide by zero (#1220) 2026-01-20 20:47:56 -08:00
14 changed files with 132 additions and 13 deletions

View File

@@ -68,7 +68,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
*.AppImage*

View File

@@ -89,7 +89,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.dmg
- name: Clean up keychain and provisioning profile

View File

@@ -60,7 +60,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
LICENSE.txt
dist/*.dmg

View File

@@ -73,6 +73,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.exe

View File

@@ -55,6 +55,6 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
generate_release_notes: false
files: |
dist/*.exe

View File

@@ -14,6 +14,8 @@ with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
**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.
@@ -238,6 +240,7 @@ MAIN:
PROCESSING:
-n, --noprocessing Do not modify image and ignore any profile or processing option
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
-u, --upscale Resize images smaller than device's resolution
-s, --stretch Stretch images to device's resolution
-r SPLITTER, --splitter SPLITTER

View File

@@ -896,6 +896,18 @@ Higher values are larger and higher quality, and may resolve blank page issues.<
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="pdfExtractBox">
<property name="toolTip">
<string>Use the PDF image extraction method from KCC 8 and earlier.
Useful for really weird PDFs.</string>
</property>
<property name="text">
<string>PDF Legacy Extract</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>

View File

@@ -327,6 +327,8 @@ class WorkerThread(QThread):
options.maximizestrips = True
if GUI.disableProcessingBox.isChecked():
options.noprocessing = True
if GUI.pdfExtractBox.isChecked():
options.pdfextract = True
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
options.metadatatitle = 1
elif GUI.metadataTitleBox.checkState() == Qt.CheckState.Checked:
@@ -1032,6 +1034,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
'colorBox': GUI.colorBox.checkState(),
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
'mozJpegBox': GUI.mozJpegBox.checkState(),
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
@@ -1195,6 +1198,9 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Kindle 1920x1920": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1920',
},
"Kindle 1240x1860": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS1240',
},
"Kindle Scribe 1/2": {
'PVOptions': True, 'ForceExpert': False, 'DefaultFormat': 0, 'DefaultUpscale': False, 'ForceColor': False, 'Label': 'KS',
},
@@ -1304,6 +1310,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
"Separator",
"Kindle 1920x1920",
"Kindle 1860x1920",
"Kindle 1240x1860",
"Kindle 8/10",
"Kindle Oasis 8",
"Kindle Paperwhite 7/10",

View File

@@ -462,6 +462,11 @@ class Ui_mainWindow(object):
self.gridLayout_2.addWidget(self.jpegQualityBox, 8, 0, 1, 1)
self.pdfExtractBox = QCheckBox(self.optionWidget)
self.pdfExtractBox.setObjectName(u"pdfExtractBox")
self.gridLayout_2.addWidget(self.pdfExtractBox, 9, 0, 1, 1)
self.gridLayout.addWidget(self.optionWidget, 5, 0, 1, 2)
@@ -733,6 +738,12 @@ class Ui_mainWindow(object):
"Higher values are larger and higher quality, and may resolve blank page issues.", None))
#endif // QT_CONFIG(tooltip)
self.jpegQualityBox.setText(QCoreApplication.translate("mainWindow", u"Custom JPEG Quality", None))
#if QT_CONFIG(tooltip)
self.pdfExtractBox.setToolTip(QCoreApplication.translate("mainWindow", u"Use the PDF image extraction method from KCC 8 and earlier.\n"
"\n"
"Useful for really weird PDFs.", None))
#endif // QT_CONFIG(tooltip)
self.pdfExtractBox.setText(QCoreApplication.translate("mainWindow", u"PDF Legacy Extract", None))
self.gammaLabel.setText(QCoreApplication.translate("mainWindow", u"Gamma: Auto", None))
self.jpegQualityLabel.setText(QCoreApplication.translate("mainWindow", u"JPEG Quality:", None))
# retranslateUi

View File

@@ -1,4 +1,4 @@
__version__ = '9.4.2'
__version__ = '9.4.3'
__license__ = 'ISC'
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
__docformat__ = 'restructuredtext en'

View File

@@ -48,6 +48,7 @@ from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
from . import comicarchive
from . import pdfjpgextract
from . import dualmetafix
from . import metadata
from . import kindle
@@ -800,9 +801,7 @@ def extract_page(vector):
if len(image_list) > 1:
raise UserWarning("mupdf_pdf_extract_page_image() function can be used only with single image pages.")
if not image_list:
width, height = int(page.rect.width), int(page.rect.height)
blank_page = Image.new("RGB", (width, height), "white")
blank_page.save(output_path)
continue
else:
xref = image_list[0][0]
d = doc.extract_image(xref)
@@ -854,6 +853,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
def getWorkFolder(afile, workdir=None):
if not workdir:
workdir = mkdtemp('', 'KCC-')
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
else:
fullPath = workdir
@@ -875,6 +875,12 @@ def getWorkFolder(afile, workdir=None):
os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
if options.pdfextract:
pdf = pdfjpgextract.PdfJpgExtract(afile, fullPath)
njpg = pdf.extract()
if njpg == 0:
raise UserWarning("Failed to extract images from PDF file.")
return workdir
target_height = options.profileData[1][1]
if options.cropping == 1:
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
@@ -1066,7 +1072,7 @@ def removeNonImages(filetree):
raise UserWarning('No images detected, nested archives are not supported.')
def sanitizeTree(filetree):
def sanitizeTree(filetree, prefix='kcc'):
chapterNames = {}
page = 1
cover_path = None
@@ -1076,7 +1082,7 @@ def sanitizeTree(filetree):
_, ext = getImageFileName(name)
# 9999 page limit
unique_name = f'kcc-{page:04}'
unique_name = f'{prefix}-{page:04}'
page += 1
newKey = os.path.join(root, unique_name + ext)
@@ -1342,6 +1348,8 @@ def makeParser():
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Do not modify image and ignore any profile or processing option")
processing_options.add_argument("--pdfextract", action="store_true", dest="pdfextract", default=False,
help="Use the legacy PDF image extraction method from KCC 8 and earlier")
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
@@ -1549,7 +1557,7 @@ def makeFusion(sources: List[str]):
else:
targetpath = fusion_path.joinpath(source_path.name)
getWorkFolder(source, str(targetpath))
sanitizeTree(targetpath)
sanitizeTree(targetpath, prefix='fusion')
# TODO: remove flattenTree when subchapters are supported
flattenTree(targetpath)

View File

@@ -103,6 +103,7 @@ class ProfileData:
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
@@ -157,7 +158,7 @@ class ComicPageParser:
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
Image.open(srcImgPath).verify()
# Image.open(srcImgPath).verify()
with Image.open(srcImgPath) as im:
self.image = im.copy()

View File

@@ -160,6 +160,8 @@ def ignore_pixels_near_edge(bw_img):
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
if not edge.height or not edge.width:
continue
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)

View File

@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
# Copyright (c) 2013-2019 Pawel Jastrzebski <pawelj@iosphe.re>
#
# Based upon the code snippet by Ned Batchelder
# (http://nedbatchelder.com/blog/200712/extracting_jpgs_from_pdfs.html)
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all
# copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
# AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
# DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
#
import os
# skip stray images a few pixels in size in some PDFs
# typical images are many thousands in length
# https://github.com/ciromattia/kcc/pull/546
STRAY_IMAGE_LENGTH_THRESHOLD = 300
class PdfJpgExtract:
def __init__(self, fname, fullPath):
self.fname = fname
self.path = fullPath
def getPath(self):
return self.path
def extract(self):
pdf = open(self.fname, "rb").read()
startmark = b"\xff\xd8"
startfix = 0
endmark = b"\xff\xd9"
endfix = 2
i = 0
njpg = 0
while True:
istream = pdf.find(b"stream", i)
if istream < 0:
break
istart = pdf.find(startmark, istream, istream + 20)
if istart < 0:
i = istream + 20
continue
iend = pdf.find(b"endstream", istart)
if iend < 0:
raise Exception("Didn't find end of stream!")
iend = pdf.find(endmark, iend - 20)
if iend < 0:
raise Exception("Didn't find end of JPG!")
istart += startfix
iend += endfix
i = iend
if iend - istart < STRAY_IMAGE_LENGTH_THRESHOLD:
continue
jpg = pdf[istart:iend]
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
jpgfile.write(jpg)
jpgfile.close()
njpg += 1
return njpg