mirror of
https://github.com/ciromattia/kcc
synced 2026-04-16 14:08:45 +00:00
Compare commits
1 Commits
master
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87acd1d7f7 |
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/kcc
|
||||
# Always creates the "latest" tag
|
||||
|
||||
2
.github/workflows/package-linux.yml
vendored
2
.github/workflows/package-linux.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
|
||||
python -m pip install --upgrade pip certifi pyinstaller --no-binary pyinstaller
|
||||
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
|
||||
python -m pip install -r requirements.txt
|
||||
- name: build binary
|
||||
run: |
|
||||
|
||||
2
.github/workflows/package-macos.yml
vendored
2
.github/workflows/package-macos.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
cache: 'pip'
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip pyinstaller certifi
|
||||
python -m pip install --upgrade pip setuptools wheel pyinstaller certifi
|
||||
pip install -r requirements.txt
|
||||
- name: Install the Apple certificate and provisioning profile
|
||||
# TODO signing
|
||||
|
||||
2
.github/workflows/package-osx-legacy.yml
vendored
2
.github/workflows/package-osx-legacy.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python3 --version
|
||||
pip3 install --upgrade pip pyinstaller certifi
|
||||
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
|
||||
pip3 install --upgrade -r requirements-osx-legacy.txt
|
||||
./gen_ui_files.sh
|
||||
- uses: actions/setup-node@v6
|
||||
|
||||
2
.github/workflows/package-windows.yml
vendored
2
.github/workflows/package-windows.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
env:
|
||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install -r requirements.txt
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
- name: build binary
|
||||
|
||||
13
.github/workflows/package-windows7.yml
vendored
13
.github/workflows/package-windows7.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
env:
|
||||
PYINSTALLER_COMPILE_BOOTLOADER: 1
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install --upgrade pip setuptools wheel
|
||||
pip install -r requirements-win7.txt
|
||||
pip install certifi pyinstaller --no-binary pyinstaller
|
||||
.\gen_ui_files.bat
|
||||
@@ -50,17 +50,6 @@ jobs:
|
||||
with:
|
||||
name: windows7-build
|
||||
path: dist/*.exe
|
||||
- id: optional_step_id
|
||||
uses: signpath/github-action-submit-signing-request@v2.0
|
||||
if: ${{ github.repository == 'ciromattia/kcc' }}
|
||||
with:
|
||||
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
|
||||
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
|
||||
project-slug: 'kcc'
|
||||
signing-policy-slug: 'release-signing'
|
||||
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: 'dist/'
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM python:3.13-slim-bullseye AS builder
|
||||
|
||||
# Install system dependencies
|
||||
RUN set -x && \
|
||||
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev" && \
|
||||
BUILD_DEPS="build-essential cmake libffi-dev libfreetype6-dev libfontconfig1-dev libpng-dev libjpeg-dev libssl-dev libxft-dev make python3-dev python3-setuptools python3-wheel" && \
|
||||
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip libgl1" && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
|
||||
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}
|
||||
|
||||
10
README.md
10
README.md
@@ -249,7 +249,6 @@ 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.
|
||||
--pdfwidth Render vector PDFs based on device width instead of height.
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
@@ -270,19 +269,13 @@ PROCESSING:
|
||||
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
|
||||
--blackborders Disable autodetection and force black borders
|
||||
--whiteborders Disable autodetection and force white borders
|
||||
--nosmartcovercrop Disable attempt to crop main cover from wide image
|
||||
--coverfill Center-crop only the cover to fill target device screen
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG for black and white images
|
||||
--webp Replace JPG with lossy WEBP and PNG with lossless WEBP
|
||||
--force-png-rgb Force color images to be saved as PNG
|
||||
--pnglegacy Use a more compatible 8 bit PNG instead of 4 bit.
|
||||
--noquantize Don't quantize PNG images to 16 colors
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--mozjpeg Create JPEG files using mozJpeg
|
||||
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
|
||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
||||
--tempdir Create temporary files directory on source file drive.
|
||||
|
||||
OUTPUT SETTINGS:
|
||||
-o OUTPUT, --output OUTPUT
|
||||
@@ -299,7 +292,6 @@ OUTPUT SETTINGS:
|
||||
Split output into multiple files. 0: Don't split 1: Automatic mode 2: Consider every subdirectory as separate volume [Default=0]
|
||||
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
|
||||
--norotate Do not rotate double page spreads in spread splitter option.
|
||||
--rotateright Rotate double page spreads in opposite direction.
|
||||
--rotatefirst Put rotated spread first in spread splitter option.
|
||||
--filefusion Combines all input files into a single file.
|
||||
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
|
||||
|
||||
1460
gui/KCC.ui
1460
gui/KCC.ui
File diff suppressed because it is too large
Load Diff
@@ -195,7 +195,7 @@ class VersionThread(QThread):
|
||||
icon = 'bindle'
|
||||
if category == 'kofi':
|
||||
icon = 'kofi'
|
||||
message = f"{payload.get('name')}"
|
||||
message = f"<b>{payload.get('name')}</b>"
|
||||
if payload.get('link'):
|
||||
message = '<a href="{}"><b>{}</b></a>'.format(payload.get('link'), payload.get('name'))
|
||||
if payload.get('showDeadline'):
|
||||
@@ -329,10 +329,6 @@ class WorkerThread(QThread):
|
||||
options.noprocessing = True
|
||||
if GUI.pdfExtractBox.isChecked():
|
||||
options.pdfextract = True
|
||||
if GUI.pdfWidthBox.isChecked():
|
||||
options.pdfwidth = True
|
||||
if GUI.noSmartCoverCropBox.isChecked():
|
||||
options.nosmartcovercrop = True
|
||||
if GUI.coverFillBox.isChecked():
|
||||
options.coverfill = True
|
||||
if GUI.metadataTitleBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
@@ -341,8 +337,6 @@ class WorkerThread(QThread):
|
||||
options.metadatatitle = 2
|
||||
if GUI.deleteBox.isChecked():
|
||||
options.delete = True
|
||||
if GUI.tempDirBox.isChecked():
|
||||
options.tempdir = True
|
||||
if GUI.spreadShiftBox.isChecked():
|
||||
options.spreadshift = True
|
||||
if GUI.fileFusionBox.isChecked():
|
||||
@@ -351,22 +345,12 @@ class WorkerThread(QThread):
|
||||
options.filefusion = False
|
||||
if GUI.noRotateBox.isChecked():
|
||||
options.norotate = True
|
||||
if GUI.rotateRightBox.isChecked():
|
||||
options.rotateright = True
|
||||
if GUI.rotateFirstBox.isChecked():
|
||||
options.rotatefirst = True
|
||||
if GUI.forcePngRgbBox.isChecked():
|
||||
options.force_png_rgb = True
|
||||
if GUI.mozJpegBox.checkState() == Qt.CheckState.PartiallyChecked:
|
||||
options.forcepng = True
|
||||
elif GUI.mozJpegBox.checkState() == Qt.CheckState.Checked:
|
||||
options.mozjpeg = True
|
||||
if GUI.webpBox.isChecked():
|
||||
options.webp = True
|
||||
if GUI.pngLegacyBox.isChecked():
|
||||
options.pnglegacy = True
|
||||
if GUI.noQuantizeBox.isChecked():
|
||||
options.noquantize = True
|
||||
if GUI.jpegQualityBox.isChecked():
|
||||
options.jpegquality = GUI.jpegQualitySpinBox.value()
|
||||
if GUI.currentMode > 2:
|
||||
@@ -698,18 +682,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
# noinspection PyCallByClass
|
||||
QDesktopServices.openUrl(QUrl('https://ko-fi.com/eink_dude'))
|
||||
|
||||
def openHumble(self):
|
||||
# noinspection PyCallByClass
|
||||
QDesktopServices.openUrl(QUrl('https://humblebundleinc.sjv.io/3JaR3A'))
|
||||
|
||||
def openYouTube(self):
|
||||
# noinspection PyCallByClass
|
||||
QDesktopServices.openUrl(QUrl('https://www.youtube.com/@eink-dude'))
|
||||
|
||||
def openDiscord(self):
|
||||
# noinspection PyCallByClass
|
||||
QDesktopServices.openUrl(QUrl('https://discord.gg/um5JRKwmGT'))
|
||||
|
||||
def modeChange(self, mode):
|
||||
if mode == 1:
|
||||
self.currentMode = 1
|
||||
@@ -844,14 +816,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if bad_format in current_format:
|
||||
self.addMessage('Scribe PNG MOBI/EPUB has a lot of problems like blank pages/sections. Use JPG instead.', 'warning')
|
||||
break
|
||||
GUI.pngLegacyBox.setEnabled(True)
|
||||
GUI.noQuantizeBox.setEnabled(True)
|
||||
GUI.forcePngRgbBox.setEnabled(True)
|
||||
else:
|
||||
GUI.pngLegacyBox.setEnabled(False)
|
||||
GUI.noQuantizeBox.setEnabled(False)
|
||||
GUI.forcePngRgbBox.setEnabled(False)
|
||||
|
||||
|
||||
def togglechunkSizeCheckBox(self, value):
|
||||
GUI.chunkSizeWidget.setVisible(value)
|
||||
@@ -919,10 +883,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
if bad_format in current_format:
|
||||
self.addMessage('Colorsoft MOBI/EPUB can have blank pages. Just go back a few pages, exit, and reenter book.', 'info')
|
||||
break
|
||||
elif profile['Label'] == 'KDX':
|
||||
GUI.mozJpegBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
GUI.borderBox.setCheckState(Qt.CheckState.PartiallyChecked)
|
||||
GUI.pngLegacyBox.setChecked(True)
|
||||
if not profile['PVOptions']:
|
||||
GUI.qualityBox.setChecked(False)
|
||||
if str(GUI.deviceBox.currentText()) == 'Other':
|
||||
@@ -949,7 +909,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
elif not GUI.webtoonBox.isChecked():
|
||||
GUI.chunkSizeCheckBox.setEnabled(True)
|
||||
if GUI.formats[str(GUI.formatBox.currentText())]['format'] in ('CBZ', 'PDF') and not GUI.webtoonBox.isChecked():
|
||||
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||
self.addMessage("Partially check W/B Margins if you don't want KCC to extend the image margins.", 'info')
|
||||
|
||||
def stripTags(self, html):
|
||||
s = HTMLStripper()
|
||||
@@ -1078,26 +1038,18 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'eraseRainbowBox': GUI.eraseRainbowBox.checkState(),
|
||||
'disableProcessingBox': GUI.disableProcessingBox.checkState(),
|
||||
'pdfExtractBox': GUI.pdfExtractBox.checkState(),
|
||||
'pdfWidthBox': GUI.pdfWidthBox.checkState(),
|
||||
'noSmartCoverCropBox': GUI.noSmartCoverCropBox.checkState(),
|
||||
'coverFillBox': GUI.coverFillBox.checkState(),
|
||||
'metadataTitleBox': GUI.metadataTitleBox.checkState(),
|
||||
'mozJpegBox': GUI.mozJpegBox.checkState(),
|
||||
'forcePngRgbBox': GUI.forcePngRgbBox.checkState(),
|
||||
'webpBox': GUI.webpBox.checkState(),
|
||||
'pngLegacyBox': GUI.pngLegacyBox.checkState(),
|
||||
'noQuantizeBox': GUI.noQuantizeBox.checkState(),
|
||||
'jpegQualityBox': GUI.jpegQualityBox.checkState(),
|
||||
'jpegQuality': GUI.jpegQualitySpinBox.value(),
|
||||
'widthBox': GUI.widthBox.value(),
|
||||
'heightBox': GUI.heightBox.value(),
|
||||
'deleteBox': GUI.deleteBox.checkState(),
|
||||
'tempDirBox': GUI.tempDirBox.checkState(),
|
||||
'spreadShiftBox': GUI.spreadShiftBox.checkState(),
|
||||
'fileFusionBox': GUI.fileFusionBox.checkState(),
|
||||
'defaultOutputFolderBox': GUI.defaultOutputFolderBox.checkState(),
|
||||
'noRotateBox': GUI.noRotateBox.checkState(),
|
||||
'rotateRightBox': GUI.rotateRightBox.checkState(),
|
||||
'rotateFirstBox': GUI.rotateFirstBox.checkState(),
|
||||
'maximizeStrips': GUI.maximizeStrips.checkState(),
|
||||
'gammaSlider': float(self.gammaValue) * 100,
|
||||
@@ -1220,7 +1172,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
'convertButton', 'formatBox']:
|
||||
getattr(GUI, element).setMinimumSize(QSize(0, 0))
|
||||
GUI.gridLayout.setContentsMargins(-1, -1, -1, -1)
|
||||
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'gridLayout_6', 'horizontalLayout_2']:
|
||||
for element in ['gridLayout_2', 'gridLayout_3', 'gridLayout_4', 'horizontalLayout', 'horizontalLayout_2']:
|
||||
getattr(GUI, element).setContentsMargins(-1, 0, -1, 0)
|
||||
if self.windowSize == '0x0':
|
||||
MW.resize(500, 500)
|
||||
@@ -1230,7 +1182,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
"EPUB": {'icon': 'EPUB', 'format': 'EPUB'},
|
||||
"CBZ": {'icon': 'CBZ', 'format': 'CBZ'},
|
||||
"PDF": {'icon': 'EPUB', 'format': 'PDF'},
|
||||
"PDF (200MB limit)": {'icon': 'EPUB', 'format': 'PDF-200MB'},
|
||||
"KFX (does not work)": {'icon': 'KFX', 'format': 'KFX'},
|
||||
"MOBI + EPUB": {'icon': 'MOBI', 'format': 'MOBI+EPUB'},
|
||||
"EPUB (200MB limit)": {'icon': 'EPUB', 'format': 'EPUB-200MB'},
|
||||
@@ -1428,9 +1379,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
GUI.editorButton.clicked.connect(self.selectFileMetaEditor)
|
||||
GUI.wikiButton.clicked.connect(self.openWiki)
|
||||
GUI.kofiButton.clicked.connect(self.openKofi)
|
||||
GUI.humbleButton.clicked.connect(self.openHumble)
|
||||
GUI.youtubeButton.clicked.connect(self.openYouTube)
|
||||
GUI.discordButton.clicked.connect(self.openDiscord)
|
||||
GUI.convertButton.clicked.connect(self.convertStart)
|
||||
GUI.gammaSlider.valueChanged.connect(self.changeGamma)
|
||||
GUI.gammaBox.stateChanged.connect(self.togglegammaBox)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
__version__ = '9.7.1'
|
||||
__version__ = '9.5.0'
|
||||
__license__ = 'ISC'
|
||||
__copyright__ = '2012-2022, Ciro Mattia Gonano <ciromattia@gmail.com>, Pawel Jastrzebski <pawelj@iosphe.re>, darodi'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
@@ -310,15 +310,6 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
f.writelines(["<dc:description>", hescape(options.summary), "</dc:description>\n"])
|
||||
for author in options.authors:
|
||||
f.writelines(["<dc:creator>", hescape(author), "</dc:creator>\n"])
|
||||
if not options.iskindle and options.series:
|
||||
f.writelines(['<meta property="belongs-to-collection" id="c02">', hescape(options.series), "</meta>\n"])
|
||||
f.writelines(['<meta refines="#c02" property="collection-type">', "series", "</meta>\n"])
|
||||
if options.volume and options.number:
|
||||
f.writelines(['<meta refines="#c02" property="group-position">', hescape(f"{options.volume}.{options.number}"), "</meta>\n"])
|
||||
elif options.volume:
|
||||
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.volume), "</meta>\n"])
|
||||
elif options.number:
|
||||
f.writelines(['<meta refines="#c02" property="group-position">', hescape(options.number), "</meta>\n"])
|
||||
f.write("<meta property=\"dcterms:modified\">" + strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + "</meta>\n")
|
||||
if cover:
|
||||
f.write("<meta name=\"cover\" content=\"cover\"/>\n")
|
||||
@@ -362,8 +353,6 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
|
||||
mt = 'image/png'
|
||||
elif '.gif' == filename[1]:
|
||||
mt = 'image/gif'
|
||||
elif '.webp' == filename[1]:
|
||||
mt = 'image/webp'
|
||||
else:
|
||||
mt = 'image/jpeg'
|
||||
f.write("<item id=\"img_" + str(uniqueid) + "\" href=\"" + folder + "/" + path[1] + "\" media-type=\"" +
|
||||
@@ -556,7 +545,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
|
||||
f.close()
|
||||
build_html_start = perf_counter()
|
||||
if cover:
|
||||
cover.save_to_folder(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
|
||||
cover.save_to_epub(os.path.join(path, 'OEBPS', 'Images', 'cover.jpg'), tomenumber, len_tomes)
|
||||
dot_clean(path)
|
||||
options.covers.append((cover, options.uuid))
|
||||
for dirpath, dirnames, filenames in os.walk(os.path.join(path, 'OEBPS', 'Images')):
|
||||
@@ -700,6 +689,7 @@ def imgFileProcessing(work):
|
||||
workImg = image.ComicPageParser((dirpath, afile), opt)
|
||||
for i in workImg.payload:
|
||||
img = image.ComicPage(opt, *i)
|
||||
is_color = (opt.forcecolor and img.color)
|
||||
if opt.cropping == 2 and not opt.webtoon:
|
||||
img.cropPageNumber(opt.croppingp, opt.croppingm)
|
||||
if opt.cropping == 1 and not opt.webtoon:
|
||||
@@ -709,24 +699,18 @@ def imgFileProcessing(work):
|
||||
|
||||
img.gammaCorrectImage()
|
||||
|
||||
if not img.colorOutput:
|
||||
img.convertToGrayscale()
|
||||
|
||||
img.autocontrastImage()
|
||||
img.resizeImage()
|
||||
img.optimizeForDisplay(opt.eraserainbow, img.colorOutput)
|
||||
img.optimizeForDisplay(opt.eraserainbow, is_color)
|
||||
|
||||
if img.colorOutput:
|
||||
if is_color:
|
||||
pass
|
||||
elif opt.forcepng:
|
||||
if not opt.noquantize:
|
||||
img.convertToGrayscale()
|
||||
if opt.format != 'PDF':
|
||||
img.quantizeImage()
|
||||
if opt.format == 'PDF':
|
||||
img.convertToGrayscale()
|
||||
elif opt.profile == 'KDX' and opt.format == 'CBZ':
|
||||
img.convertToGrayscale()
|
||||
elif opt.pnglegacy:
|
||||
img.convertToGrayscale()
|
||||
else:
|
||||
img.convertToGrayscale()
|
||||
output.append(img.saveToDir())
|
||||
return output
|
||||
except Exception:
|
||||
@@ -756,9 +740,7 @@ def render_page(vector):
|
||||
cpu = vector[1] # number of CPUs
|
||||
filename = vector[2] # document filename
|
||||
output_dir = vector[3]
|
||||
target_width = vector[4]
|
||||
target_height = vector[5]
|
||||
pdf_width = vector[6]
|
||||
target_height = vector[4]
|
||||
with pymupdf.open(filename) as doc: # open the document
|
||||
num_pages = doc.page_count # get number of pages
|
||||
|
||||
@@ -769,10 +751,7 @@ def render_page(vector):
|
||||
|
||||
for i in range(seg_from, seg_to): # work through our page segment
|
||||
page = doc[i]
|
||||
if not pdf_width or page.rect.width > page.rect.height:
|
||||
zoom = target_height / page.rect.height
|
||||
else:
|
||||
zoom = target_width / page.rect.width
|
||||
zoom = target_height / page.rect.height
|
||||
mat = pymupdf.Matrix(zoom, zoom)
|
||||
# TODO: decide colorspace earlier so later color check is cheaper.
|
||||
# This is actually pretty hard when you have to deal with color vector text
|
||||
@@ -837,7 +816,7 @@ def extract_page(vector):
|
||||
|
||||
|
||||
|
||||
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_height):
|
||||
def mupdf_pdf_process_pages_parallel(filename, output_dir, target_height):
|
||||
render = False
|
||||
with pymupdf.open(filename) as doc:
|
||||
for page in doc:
|
||||
@@ -857,7 +836,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
||||
cpu = cpu_count()
|
||||
|
||||
# make vectors of arguments for the processes
|
||||
vectors = [(i, cpu, filename, output_dir, target_width, target_height, options.pdfwidth) for i in range(cpu)]
|
||||
vectors = [(i, cpu, filename, output_dir, target_height) for i in range(cpu)]
|
||||
print("Starting %i processes for '%s'." % (cpu, filename))
|
||||
|
||||
|
||||
@@ -874,8 +853,7 @@ def mupdf_pdf_process_pages_parallel(filename, output_dir, target_width, target_
|
||||
def getWorkFolder(afile, workdir=None):
|
||||
if not workdir:
|
||||
workdir = mkdtemp('', 'KCC-')
|
||||
if options.tempdir:
|
||||
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
# workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
|
||||
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
|
||||
else:
|
||||
fullPath = workdir
|
||||
@@ -903,15 +881,13 @@ def getWorkFolder(afile, workdir=None):
|
||||
if njpg == 0:
|
||||
raise UserWarning("Failed to extract images from PDF file.")
|
||||
return workdir
|
||||
target_width, target_height = options.profileData[1]
|
||||
target_height = options.profileData[1][1]
|
||||
if options.cropping == 1:
|
||||
target_height *= 1.2 #Account for possible margin at the top and bottom
|
||||
target_width *= 1.2
|
||||
target_height = target_height + target_height*0.20 #Account for possible margin at the top and bottom
|
||||
elif options.cropping == 2:
|
||||
target_height *= 1.25 #Account for possible margin at the top and bottom with page number
|
||||
target_width *= 1.25
|
||||
target_height = target_height + target_height*0.25 #Account for possible margin at the top and bottom with page number
|
||||
try:
|
||||
mupdf_pdf_process_pages_parallel(afile, fullPath, target_width, target_height)
|
||||
mupdf_pdf_process_pages_parallel(afile, fullPath, target_height)
|
||||
except Exception as e:
|
||||
rmtree(path, True)
|
||||
raise UserWarning(f"Failed to extract images from PDF file. {e}")
|
||||
@@ -1002,9 +978,6 @@ def getMetadata(path, originalpath):
|
||||
options.comicinfo_chapters = []
|
||||
options.summary = ''
|
||||
titleSuffix = ''
|
||||
options.volume = ''
|
||||
options.number = ''
|
||||
options.series = ''
|
||||
if options.title == 'defaulttitle':
|
||||
defaultTitle = True
|
||||
if os.path.isdir(originalpath):
|
||||
@@ -1033,10 +1006,8 @@ def getMetadata(path, originalpath):
|
||||
options.title = xml.data['Series']
|
||||
if xml.data['Volume']:
|
||||
titleSuffix += ' Vol. ' + xml.data['Volume'].zfill(2)
|
||||
options.volume = xml.data['Volume']
|
||||
if xml.data['Number']:
|
||||
titleSuffix += ' #' + xml.data['Number'].zfill(3)
|
||||
options.number = xml.data['Number']
|
||||
if options.metadatatitle == 1 and xml.data['Title']:
|
||||
titleSuffix += ': ' + xml.data['Title']
|
||||
options.title += titleSuffix
|
||||
@@ -1054,8 +1025,6 @@ def getMetadata(path, originalpath):
|
||||
options.comicinfo_chapters = xml.data['Bookmarks']
|
||||
if xml.data['Summary']:
|
||||
options.summary = xml.data['Summary']
|
||||
if xml.data['Series']:
|
||||
options.series = xml.data['Series']
|
||||
os.remove(xmlPath)
|
||||
|
||||
if originalpath.lower().endswith('.pdf'):
|
||||
@@ -1316,12 +1285,12 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
|
||||
mimetypeFile = open(os.path.join(basedir, '!mimetype'), 'w')
|
||||
mimetypeFile.write('application/epub+zip')
|
||||
mimetypeFile.close()
|
||||
subprocess_run([SEVENZIP, 'a', '-mx0', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
|
||||
subprocess_run([SEVENZIP, 'a', '-tzip', zipfilename, "*"], capture_output=True, check=True, cwd=basedir)
|
||||
# crazy hack to ensure mimetype is first when using 7zip
|
||||
if isepub:
|
||||
subprocess_run([SEVENZIP, 'rn', zipfilename, '!mimetype', 'mimetype'], capture_output=True, check=True, cwd=basedir)
|
||||
else:
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_STORED)
|
||||
zipOutput = ZipFile(zipfilename, 'w', ZIP_DEFLATED)
|
||||
if isepub:
|
||||
zipOutput.writestr('mimetype', 'application/epub+zip', ZIP_STORED)
|
||||
for dirpath, _, filenames in os.walk(basedir):
|
||||
@@ -1384,8 +1353,6 @@ def makeParser():
|
||||
help="Shift first page to opposite side in landscape for spread alignment")
|
||||
output_options.add_argument("--norotate", action="store_true", dest="norotate", default=False,
|
||||
help="Do not rotate double page spreads in spread splitter option.")
|
||||
output_options.add_argument("--rotateright", action="store_true", dest="rotateright", default=False,
|
||||
help="Rotate double page spreads in opposite direction.")
|
||||
output_options.add_argument("--rotatefirst", action="store_true", dest="rotatefirst", default=False,
|
||||
help="Put rotated 2 page spread first in spread splitter option.")
|
||||
|
||||
@@ -1393,10 +1360,6 @@ def makeParser():
|
||||
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("--pdfwidth", action="store_true", dest="pdfwidth", default=False,
|
||||
help="Render vector PDFs to device width instead of height.")
|
||||
processing_options.add_argument("--nosmartcovercrop", action="store_true", dest="nosmartcovercrop", default=False,
|
||||
help="Disable attempt to crop main cover from wide image")
|
||||
processing_options.add_argument("--coverfill", action="store_true", dest="coverfill", default=False,
|
||||
help="Crop cover to fill screen")
|
||||
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||
@@ -1434,15 +1397,7 @@ def makeParser():
|
||||
output_options.add_argument("--eraserainbow", action="store_true", dest="eraserainbow", default=False,
|
||||
help="Erase rainbow effect on color eink screen by attenuating interfering frequencies")
|
||||
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
|
||||
help="Create PNG files instead JPEG for black and white images")
|
||||
processing_options.add_argument("--force-png-rgb", action="store_true", dest="force_png_rgb", default=False,
|
||||
help="Force color images to be saved as PNG")
|
||||
processing_options.add_argument("--webp", action="store_true", dest="webp", default=False,
|
||||
help="Replace JPG with lossy WEBP and PNG with lossless WEBP")
|
||||
processing_options.add_argument("--pnglegacy", action="store_true", dest="pnglegacy", default=False,
|
||||
help="Use a more compatible 8 bit png instead of 4 bit")
|
||||
processing_options.add_argument("--noquantize", action="store_true", dest="noquantize", default=False,
|
||||
help="Don't quantize to 16 color PNG")
|
||||
help="Create PNG files instead JPEG")
|
||||
processing_options.add_argument("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
|
||||
help="Create JPEG files using mozJpeg")
|
||||
processing_options.add_argument("--jpeg-quality", type=int, dest="jpegquality",
|
||||
@@ -1451,8 +1406,6 @@ def makeParser():
|
||||
help="Turn 1x4 strips to 2x2 strips")
|
||||
processing_options.add_argument("-d", "--delete", action="store_true", dest="delete", default=False,
|
||||
help="Delete source file(s) or a directory. It's not recoverable.")
|
||||
processing_options.add_argument("--tempdir", action="store_true", dest="tempdir", default=False,
|
||||
help="Create temporary files directory on source file drive.")
|
||||
|
||||
custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
|
||||
help="Replace screen width provided by device profile")
|
||||
@@ -1471,11 +1424,6 @@ def checkOptions(options):
|
||||
options.isKobo = False
|
||||
options.bordersColor = None
|
||||
options.keep_epub = False
|
||||
if options.format == 'PDF-200MB':
|
||||
options.targetsize = 195
|
||||
options.format = 'PDF'
|
||||
if options.batchsplit != 2:
|
||||
options.batchsplit = 1
|
||||
if options.format == 'EPUB-200MB':
|
||||
options.targetsize = 195
|
||||
options.format = 'EPUB'
|
||||
@@ -1534,6 +1482,9 @@ def checkOptions(options):
|
||||
if 'Ko' in options.profile:
|
||||
options.panelview = False
|
||||
options.hq = False
|
||||
# CBZ files on Kindle DX/DXG support higher resolution
|
||||
if options.profile == 'KDX' and options.format == 'CBZ':
|
||||
options.customheight = 1200
|
||||
# KFX output create EPUB that might be can be by jhowell KFX Output Calibre plugin
|
||||
if options.format == 'KFX':
|
||||
options.format = 'EPUB'
|
||||
@@ -1559,15 +1510,6 @@ def checkOptions(options):
|
||||
options.jpegquality = 85
|
||||
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
|
||||
options.kindle_scribe_azw3 = options.profile.startswith('KS') and options.kindle_azw3
|
||||
|
||||
options.webp_output = options.format != 'PDF' and not options.kindle_azw3 and options.webp
|
||||
|
||||
# CBZ files on Kindle DX/DXG support higher resolution
|
||||
if options.profile == 'KDX' and options.format == 'CBZ':
|
||||
options.profileData = list(image.ProfileData.Profiles[options.profile])
|
||||
options.profileData[1] = list(options.profileData[1])
|
||||
options.profileData[1][1] = 1200
|
||||
|
||||
if options.kindle_scribe_azw3:
|
||||
options.profileData = list(image.ProfileData.Profiles[options.profile])
|
||||
options.profileData[1] = list(options.profileData[1])
|
||||
@@ -1715,16 +1657,12 @@ def makeBook(source, qtgui=None, job_progress=''):
|
||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
|
||||
else:
|
||||
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
|
||||
if cover.smartcover:
|
||||
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
|
||||
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
|
||||
elif options.format == 'PDF':
|
||||
print(f"{job_progress}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)
|
||||
if cover.smartcover:
|
||||
cover.save_to_folder(os.path.join(tome, 'OEBPS', 'Images', 'cover.jpg'), tomeNumber, len(tomes))
|
||||
# use optimized buildPDF logic with streaming and compression
|
||||
output_pdf = buildPDF(tome, options.title, job_progress, None, output_file)
|
||||
filepath.append(output_pdf)
|
||||
|
||||
@@ -197,13 +197,10 @@ class ComicPageParser:
|
||||
and not self.opt.webtoon and self.opt.splitter == 1:
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
if not self.opt.rotateright:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
else:
|
||||
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
|
||||
if self.opt.splitter != 1 and width / height < 2:
|
||||
if self.opt.splitter != 1:
|
||||
if width > height:
|
||||
leftbox = (0, 0, int(width / 2), height)
|
||||
rightbox = (int(width / 2), 0, width, height)
|
||||
@@ -218,13 +215,10 @@ class ComicPageParser:
|
||||
pagetwo = self.image.crop(rightbox)
|
||||
self.payload.append(['S1', self.source, pageone, self.fill])
|
||||
self.payload.append(['S2', self.source, pagetwo, self.fill])
|
||||
if self.opt.splitter > 0 or (self.opt.splitter == 0 and width / height >= 2):
|
||||
if self.opt.splitter > 0:
|
||||
spread = self.image
|
||||
if not self.opt.norotate:
|
||||
if not self.opt.rotateright:
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
else:
|
||||
spread = spread.rotate(-90, Image.Resampling.BICUBIC, True)
|
||||
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
|
||||
self.payload.append(['R', self.source, spread, self.fill])
|
||||
else:
|
||||
self.payload.append(['N', self.source, self.image, self.fill])
|
||||
@@ -277,8 +271,6 @@ class ComicPage:
|
||||
self.original_color_mode = image.mode
|
||||
# TODO: color check earlier
|
||||
self.image = image.convert("RGB")
|
||||
self.color = self.colorCheck()
|
||||
self.colorOutput = self.color and self.opt.forcecolor
|
||||
self.fill = fill
|
||||
self.rotated = False
|
||||
self.orgPath = os.path.join(path[0], path[1])
|
||||
@@ -297,7 +289,8 @@ class ComicPage:
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
|
||||
def colorCheck(self):
|
||||
@cached_property
|
||||
def color(self):
|
||||
if self.original_color_mode in ("L", "1"):
|
||||
return False
|
||||
if self.opt.webtoon:
|
||||
@@ -406,23 +399,17 @@ class ComicPage:
|
||||
raise RuntimeError('Cannot save image. ' + str(err))
|
||||
|
||||
def save_with_codec(self, image, targetPath):
|
||||
if self.opt.forcepng and (not self.colorOutput or self.opt.force_png_rgb):
|
||||
if self.opt.forcepng:
|
||||
image.info.pop('transparency', None)
|
||||
if self.opt.webp_output:
|
||||
targetPath += '.webp'
|
||||
image.save(targetPath, 'WEBP', lossless=True, quality=self.opt.jpegquality)
|
||||
elif self.opt.kindle_azw3:
|
||||
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
|
||||
targetPath += '.gif'
|
||||
image.save(targetPath, 'GIF', optimize=1, interlace=False)
|
||||
else:
|
||||
targetPath += '.png'
|
||||
image.save(targetPath, 'PNG', optimize=1)
|
||||
else:
|
||||
if self.opt.webp_output:
|
||||
targetPath += '.webp'
|
||||
image.save(targetPath, 'WEBP', quality=self.opt.jpegquality)
|
||||
elif self.opt.mozjpeg:
|
||||
targetPath += '.jpg'
|
||||
targetPath += '.jpg'
|
||||
if self.opt.mozjpeg:
|
||||
with io.BytesIO() as output:
|
||||
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
|
||||
input_jpeg_bytes = output.getvalue()
|
||||
@@ -430,7 +417,6 @@ class ComicPage:
|
||||
with open(targetPath, "wb") as output_jpeg_file:
|
||||
output_jpeg_file.write(output_jpeg_bytes)
|
||||
else:
|
||||
targetPath += '.jpg'
|
||||
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
|
||||
return targetPath
|
||||
|
||||
@@ -515,9 +501,7 @@ class ComicPage:
|
||||
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
|
||||
pass
|
||||
else: # if image bigger than device resolution or smaller with upscaling
|
||||
if self.opt.profile == 'KDX' and abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD * 3:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif (self.opt.format in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
|
||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||
@@ -525,7 +509,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
|
||||
@@ -569,7 +553,6 @@ class Cover:
|
||||
self.options = opt
|
||||
self.source = source
|
||||
self.image = Image.open(source)
|
||||
self.smartcover = False
|
||||
# backwards compatibility for Pillow >9.1.0
|
||||
if not hasattr(Image, 'Resampling'):
|
||||
Image.Resampling = Image
|
||||
@@ -580,8 +563,7 @@ class Cover:
|
||||
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
|
||||
if not self.options.forcecolor:
|
||||
self.image = self.image.convert('L')
|
||||
if not self.options.nosmartcovercrop:
|
||||
self.crop_main_cover()
|
||||
self.crop_main_cover()
|
||||
|
||||
size = list(self.options.profileData[1])
|
||||
if self.options.kindle_scribe_azw3:
|
||||
@@ -596,19 +578,17 @@ class Cover:
|
||||
def crop_main_cover(self):
|
||||
w, h = self.image.size
|
||||
if w / h > 2:
|
||||
self.smartcover = True
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
|
||||
else:
|
||||
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
|
||||
elif w / h > 1.34:
|
||||
self.smartcover = True
|
||||
if self.options.righttoleft:
|
||||
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
|
||||
else:
|
||||
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
|
||||
|
||||
def save_to_folder(self, target, tomeid, len_tomes=0):
|
||||
def save_to_epub(self, target, tomeid, len_tomes=0):
|
||||
try:
|
||||
if tomeid == 0:
|
||||
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
PySide6<6.10
|
||||
PySide6>6
|
||||
Pillow>=11.3.0
|
||||
psutil>=5.9.5
|
||||
requests>=2.31.0
|
||||
|
||||
Reference in New Issue
Block a user