1
0
mirror of https://github.com/ciromattia/kcc synced 2026-04-15 21:48:44 +00:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Alex Xu
d2981c0ceb Bump version to 9.3.0 2025-11-09 18:07:50 -08:00
José Cerezo
8268552ac7 Optimized Docker image (#1155)
* Optimized Docker image

* Divided Dockerfile into two images

* Fixed dockerfile path

* Updated workflows

* Added remaining packages in Dockerfile-base

* Updated workflows
2025-11-09 15:58:55 -08:00
26 changed files with 221 additions and 285 deletions

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -0,0 +1,45 @@
name: Build and Publish Base Docker Image
on:
workflow_dispatch:
push:
branches:
- master
paths:
- 'requirements-docker.txt'
- 'Dockerfile-base'
jobs:
build_and_publish_base_image:
runs-on: ubuntu-latest
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set Release Date
id: release_date
run: |
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
file: Dockerfile-base
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/kcc:base-latest
ghcr.io/${{ github.repository_owner }}/kcc:base-${{ steps.release_date.outputs.release_date }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:base-cache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:base-cache,mode=max

View File

@@ -1,4 +1,4 @@
name: Build and Publish Docker Image
name: Build and publish final image
on:
workflow_dispatch:
@@ -6,7 +6,7 @@ on:
tags:
- 'v*.*.*'
# Don't trigger if it's just a documentation update
# Don't trigger if it's just a documentation update or a base image update
paths-ignore:
- '**.md'
- '**.MD'
@@ -15,14 +15,13 @@ on:
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- 'requirements-docker.txt'
- 'Dockerfile-base'
jobs:
build_and_publish_base_image:
build_and_publish_image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
@@ -36,32 +35,17 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set Release Date
id: release_date
run: |
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/kcc
# Always creates the "latest" tag
flavor: |
latest=true
tags: |
type=ref,event=tag
type=raw,value=${{ steps.release_date.outputs.release_date }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: true
tags: |
${{ steps.meta.outputs.tags }}
cache-from: |
type=registry,ref=ghcr.io/ciromattia/kcc:cache
type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:final-cache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:final-cache,mode=max

View File

@@ -25,7 +25,7 @@ jobs:
build:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -59,7 +59,7 @@ jobs:
env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: AppImage
path: './*.AppImage*'

View File

@@ -25,12 +25,10 @@ jobs:
build:
strategy:
matrix:
os: [ macos-15-intel, macos-14 ]
os: [ macos-13, macos-14 ]
runs-on: ${{ matrix.os }}
env:
MACOSX_DEPLOYMENT_TARGET: '12'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -80,7 +78,7 @@ jobs:
run: |
python setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: mac-os-build-${{ runner.arch }}
path: dist/*.dmg

View File

@@ -23,7 +23,7 @@ jobs:
build:
strategy:
matrix:
os: [ macos-15-intel ]
os: [ macos-13 ]
runs-on: ${{ matrix.os }}
env:
# We need the official Python, because the GA ones only support newer macOS versions
@@ -31,7 +31,7 @@ jobs:
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Get Python
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
- name: Install Python
@@ -51,7 +51,7 @@ jobs:
run: |
python3 setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg

View File

@@ -35,7 +35,7 @@ jobs:
command: build_c2p
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -53,12 +53,12 @@ jobs:
python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: windows-build-${{ matrix.entry }}
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0
uses: signpath/github-action-submit-signing-request@v1.3
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'

View File

@@ -27,7 +27,7 @@ jobs:
env:
WINDOWS_7: 1
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
@@ -46,7 +46,7 @@ jobs:
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v4
with:
name: windows7-build
path: dist/*.exe

View File

@@ -1,77 +1,20 @@
# STAGE 1: BUILDER
# Contains all build tools and dev dependencies, will be discarded
FROM python:3.13-slim-bullseye AS builder
FROM ghcr.io/ciromattia/kcc:base-latest
# 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 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}
RUN \
set -x && \
python -m venv /opt/venv && \
. /opt/venv/bin/activate && \
pip install --upgrade pip
# Install numpy first, as it is unlikely to change and takes too long to compile
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir numpy==2.3.4
# Install PyMuPDF separately, as it is likely to change but still takes too long to compile
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir PyMuPDF==1.26.6
# Install Python dependencies using virtual environment
COPY requirements-docker.txt .
RUN \
set -x && \
. /opt/venv/bin/activate && \
pip install --no-cache-dir -r requirements-docker.txt
# STAGE 2: FINAL
# Clean, small and secure image with only runtime dependencies
FROM python:3.13-slim-bullseye
# Install runtime dependencies only
RUN \
set -x && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends p7zip-full && \
rm -rf /var/lib/apt/lists/*
# Copy artifacts from builder
COPY --from=builder /opt/venv /opt/venv
COPY . /opt/kcc/
WORKDIR /opt/kcc
ENV PATH="/opt/venv/bin:$PATH"
# Setup executable and version file
RUN \
chmod +x /opt/kcc/entrypoint.sh && \
ln -s /opt/kcc/kcc-c2e.py /usr/local/bin/c2e && \
ln -s /opt/kcc/kcc-c2p.py /usr/local/bin/c2p && \
ln -s /opt/kcc/entrypoint.sh /usr/local/bin/entrypoint && \
ln -s /opt/kcc/kindlegen/kindlegen /usr/local/bin/kindlegen && \
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
LABEL com.kcc.name="Kindle Comic Converter" \
com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" \
org.opencontainers.image.title="Kindle Comic Converter" \
org.opencontainers.image.description='Kindle Comic Converter' \
org.opencontainers.image.documentation='https://github.com/ciromattia/kcc' \
org.opencontainers.image.source='https://github.com/ciromattia/kcc' \
org.opencontainers.image.authors='Darodi and José Cerezo' \
org.opencontainers.image.url='https://github.com/ciromattia/kcc' \
org.opencontainers.image.vendor='ciromattia' \
org.opencontainers.image.licenses='ISC'
org.opencontainers.image.title="Kindle Comic Converter"
ENTRYPOINT ["entrypoint"]
CMD ["-h"]
CMD ["-h"]

44
Dockerfile-base Normal file
View File

@@ -0,0 +1,44 @@
# STAGE 1: BUILDER
# Contains all build tools and dev dependencies, will be discarded
FROM python:3.13-slim-bullseye AS builder
ARG TARGETARCH
# 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 python3-setuptools python3-wheel" && \
RUNTIME_DEPS="bash ca-certificates chrpath locales locales-all libfreetype6 libfontconfig1 p7zip-full python3 python3-pip unrar-free libgl1" && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends ${BUILD_DEPS} ${RUNTIME_DEPS}
# Install Python dependencies using virtual environment
COPY requirements-docker.txt .
RUN \
set -x && \
python -m venv /opt/venv && \
. /opt/venv/bin/activate && \
pip install --upgrade pip && \
pip install --no-cache-dir -r requirements-docker.txt
# STAGE 2: FINAL
# Clean, small and secure image with only runtime dependencies
FROM python:3.13-slim-bullseye
# Install runtime dependencies only
RUN \
set -x && \
DEBIAN_FRONTEND=noninteractive apt-get update -y && \
apt-get install -y --no-install-recommends p7zip-full unrar-free && \
rm -rf /var/lib/apt/lists/*
# Copy artifacts from builder
COPY --from=builder /opt/venv /opt/venv
WORKDIR /opt/kcc
ENV PATH="/opt/venv/bin:$PATH"
LABEL com.kcc.name="Kindle Comic Converter" \
com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" \
org.opencontainers.image.description='Kindle Comic Converter Base Image' \
org.opencontainers.image.source='https://github.com/ciromattia/kcc' \
org.opencontainers.image.title="Kindle Comic Converter Base Image"

View File

@@ -7,7 +7,7 @@
[![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](https://github.com/ciromattia/kcc/releases)
**Kindle Comic Converter** optimizes black & white or color comics and manga for E-ink ereaders
**Kindle Comic Converter** optimizes black & white comics and manga for E-ink ereaders
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
@@ -26,7 +26,7 @@ which have different requirements than normal LCD screens.
Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
on underpowered ereaders with small storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
@@ -104,7 +104,7 @@ For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.co
## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre will break the formatting.
Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
@@ -318,7 +318,6 @@ Depending on your system [Python](https://www.python.org) may be called either `
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 `pyside6-designer` which is included in the `pip install pyside6`.
If new objects have been added, verify that correct tab order has been applied by using [Tab Order Editing Mode](https://doc.qt.io/qt-6/designer-tab-order.html).
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

View File

@@ -65,7 +65,7 @@
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p style='white-space:pre'&gt;Enable right-to-left reading.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Right-to-left (manga)</string>
<string>Right-to-left mode</string>
</property>
</widget>
</item>
@@ -898,47 +898,36 @@
</widget>
</widget>
<tabstops>
<tabstop>jobList</tabstop>
<tabstop>fileButton</tabstop>
<tabstop>clearButton</tabstop>
<tabstop>defaultOutputFolderButton</tabstop>
<tabstop>defaultOutputFolderBox</tabstop>
<tabstop>deviceBox</tabstop>
<tabstop>widthBox</tabstop>
<tabstop>heightBox</tabstop>
<tabstop>formatBox</tabstop>
<tabstop>convertButton</tabstop>
<tabstop>clearButton</tabstop>
<tabstop>deviceBox</tabstop>
<tabstop>formatBox</tabstop>
<tabstop>mangaBox</tabstop>
<tabstop>rotateBox</tabstop>
<tabstop>qualityBox</tabstop>
<tabstop>webtoonBox</tabstop>
<tabstop>upscaleBox</tabstop>
<tabstop>gammaBox</tabstop>
<tabstop>gammaSlider</tabstop>
<tabstop>borderBox</tabstop>
<tabstop>outputSplit</tabstop>
<tabstop>colorBox</tabstop>
<tabstop>mozJpegBox</tabstop>
<tabstop>maximizeStrips</tabstop>
<tabstop>croppingBox</tabstop>
<tabstop>croppingPowerSlider</tabstop>
<tabstop>preserveMarginBox</tabstop>
<tabstop>spreadShiftBox</tabstop>
<tabstop>deleteBox</tabstop>
<tabstop>disableProcessingBox</tabstop>
<tabstop>fileFusionBox</tabstop>
<tabstop>chunkSizeBox</tabstop>
<tabstop>noRotateBox</tabstop>
<tabstop>interPanelCropBox</tabstop>
<tabstop>metadataTitleBox</tabstop>
<tabstop>chunkSizeCheckBox</tabstop>
<tabstop>chunkSizeBox</tabstop>
<tabstop>eraseRainbowBox</tabstop>
<tabstop>rotateFirstBox</tabstop>
<tabstop>autoLevelBox</tabstop>
<tabstop>autocontrastBox</tabstop>
<tabstop>heightBox</tabstop>
<tabstop>croppingPowerSlider</tabstop>
<tabstop>editorButton</tabstop>
<tabstop>kofiButton</tabstop>
<tabstop>wikiButton</tabstop>
<tabstop>jobList</tabstop>
<tabstop>gammaSlider</tabstop>
<tabstop>widthBox</tabstop>
</tabstops>
<resources>
<include location="KCC.qrc"/>

View File

@@ -192,18 +192,6 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>
<tabstop>pencillerLine</tabstop>
<tabstop>inkerLine</tabstop>
<tabstop>coloristLine</tabstop>
<tabstop>okButton</tabstop>
<tabstop>cancelButton</tabstop>
</tabstops>
<resources>
<include location="KCC.qrc"/>
</resources>

View File

@@ -382,14 +382,13 @@ class WorkerThread(QThread):
error_message = 'Process Failed. Custom title can\'t be set when processing more than 1 source.\nDid you forget to check fusion?'
print(error_message)
MW.addMessage.emit(error_message, 'error', True)
for i, job in enumerate(currentJobs, start=1):
job_progress_number = f'[{i}/{len(currentJobs)}] '
for job in currentJobs:
sleep(0.5)
if not self.conversionAlive:
self.clean()
return
self.errors = False
MW.addMessage.emit(f'<b>{job_progress_number}Source:</b> ' + job, 'info', False)
MW.addMessage.emit('<b>Source:</b> ' + job, 'info', False)
if gui_current_format == 'CBZ':
MW.addMessage.emit('Creating CBZ files', 'info', False)
GUI.progress.content = 'Creating CBZ files'
@@ -403,7 +402,7 @@ class WorkerThread(QThread):
jobargv.append(job)
try:
comic2ebook.options = comic2ebook.checkOptions(copy(options))
outputPath = comic2ebook.makeBook(job, self, job_progress_number)
outputPath = comic2ebook.makeBook(job, self)
MW.hideProgressBar.emit()
except UserWarning as warn:
if not self.conversionAlive:
@@ -445,7 +444,7 @@ class WorkerThread(QThread):
else:
MW.addMessage.emit('Creating EPUB files... <b>Done!</b>', 'info', True)
if 'MOBI' in gui_current_format:
MW.progressBarTick.emit(f'{job_progress_number}Creating MOBI files')
MW.progressBarTick.emit('Creating MOBI files')
MW.progressBarTick.emit(str(len(outputPath) * 2 + 1))
MW.progressBarTick.emit('tick')
MW.addMessage.emit('Creating MOBI files', 'info', False)
@@ -1028,7 +1027,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
MW.activateWindow()
if type(message) is bytes:
message = message.decode('UTF-8')
if not self.conversionAlive and message != 'ARISE' and not GUI.jobList.findItems(message, Qt.MatchFlag.MatchExactly):
if not self.conversionAlive and message != 'ARISE':
if self.needClean:
self.needClean = False
GUI.jobList.clear()
@@ -1059,7 +1058,6 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
if message[-1] == '/':
message = message[:-1]
self.handleMessage(message)
GUI.jobList.sortItems()
def forceShutdown(self):
self.saveSettings(None)

View File

@@ -469,46 +469,35 @@ class Ui_mainWindow(object):
self.statusBar.setObjectName(u"statusBar")
self.statusBar.setSizeGripEnabled(False)
mainWindow.setStatusBar(self.statusBar)
QWidget.setTabOrder(self.jobList, self.fileButton)
QWidget.setTabOrder(self.fileButton, self.clearButton)
QWidget.setTabOrder(self.clearButton, self.defaultOutputFolderButton)
QWidget.setTabOrder(self.defaultOutputFolderButton, self.defaultOutputFolderBox)
QWidget.setTabOrder(self.defaultOutputFolderBox, self.deviceBox)
QWidget.setTabOrder(self.deviceBox, self.widthBox)
QWidget.setTabOrder(self.widthBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.formatBox)
QWidget.setTabOrder(self.formatBox, self.convertButton)
QWidget.setTabOrder(self.convertButton, self.mangaBox)
QWidget.setTabOrder(self.convertButton, self.clearButton)
QWidget.setTabOrder(self.clearButton, self.deviceBox)
QWidget.setTabOrder(self.deviceBox, self.formatBox)
QWidget.setTabOrder(self.formatBox, self.mangaBox)
QWidget.setTabOrder(self.mangaBox, self.rotateBox)
QWidget.setTabOrder(self.rotateBox, self.qualityBox)
QWidget.setTabOrder(self.qualityBox, self.webtoonBox)
QWidget.setTabOrder(self.webtoonBox, self.upscaleBox)
QWidget.setTabOrder(self.upscaleBox, self.gammaBox)
QWidget.setTabOrder(self.gammaBox, self.gammaSlider)
QWidget.setTabOrder(self.gammaSlider, self.borderBox)
QWidget.setTabOrder(self.gammaBox, self.borderBox)
QWidget.setTabOrder(self.borderBox, self.outputSplit)
QWidget.setTabOrder(self.outputSplit, self.colorBox)
QWidget.setTabOrder(self.colorBox, self.mozJpegBox)
QWidget.setTabOrder(self.mozJpegBox, self.maximizeStrips)
QWidget.setTabOrder(self.maximizeStrips, self.croppingBox)
QWidget.setTabOrder(self.croppingBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.preserveMarginBox)
QWidget.setTabOrder(self.preserveMarginBox, self.spreadShiftBox)
QWidget.setTabOrder(self.croppingBox, self.spreadShiftBox)
QWidget.setTabOrder(self.spreadShiftBox, self.deleteBox)
QWidget.setTabOrder(self.deleteBox, self.disableProcessingBox)
QWidget.setTabOrder(self.disableProcessingBox, self.fileFusionBox)
QWidget.setTabOrder(self.fileFusionBox, self.noRotateBox)
QWidget.setTabOrder(self.disableProcessingBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.noRotateBox)
QWidget.setTabOrder(self.noRotateBox, self.interPanelCropBox)
QWidget.setTabOrder(self.interPanelCropBox, self.metadataTitleBox)
QWidget.setTabOrder(self.metadataTitleBox, self.chunkSizeCheckBox)
QWidget.setTabOrder(self.chunkSizeCheckBox, self.chunkSizeBox)
QWidget.setTabOrder(self.chunkSizeBox, self.eraseRainbowBox)
QWidget.setTabOrder(self.eraseRainbowBox, self.rotateFirstBox)
QWidget.setTabOrder(self.rotateFirstBox, self.autoLevelBox)
QWidget.setTabOrder(self.autoLevelBox, self.autocontrastBox)
QWidget.setTabOrder(self.autocontrastBox, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.kofiButton)
QWidget.setTabOrder(self.kofiButton, self.wikiButton)
QWidget.setTabOrder(self.interPanelCropBox, self.eraseRainbowBox)
QWidget.setTabOrder(self.eraseRainbowBox, self.heightBox)
QWidget.setTabOrder(self.heightBox, self.croppingPowerSlider)
QWidget.setTabOrder(self.croppingPowerSlider, self.editorButton)
QWidget.setTabOrder(self.editorButton, self.wikiButton)
QWidget.setTabOrder(self.wikiButton, self.jobList)
QWidget.setTabOrder(self.jobList, self.gammaSlider)
QWidget.setTabOrder(self.gammaSlider, self.widthBox)
self.retranslateUi(mainWindow)
@@ -524,7 +513,7 @@ class Ui_mainWindow(object):
#if QT_CONFIG(tooltip)
self.mangaBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable right-to-left reading.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left (manga)", None))
self.mangaBox.setText(QCoreApplication.translate("mainWindow", u"Right-to-left mode", None))
#if QT_CONFIG(tooltip)
self.webtoonBox.setToolTip(QCoreApplication.translate("mainWindow", u"<html><head/><body><p style='white-space:pre'>Enable special parsing mode for Korean Webtoons.</p></body></html>", None))
#endif // QT_CONFIG(tooltip)

View File

@@ -156,15 +156,6 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
QWidget.setTabOrder(self.pencillerLine, self.inkerLine)
QWidget.setTabOrder(self.inkerLine, self.coloristLine)
QWidget.setTabOrder(self.coloristLine, self.okButton)
QWidget.setTabOrder(self.okButton, self.cancelButton)
self.retranslateUi(editorDialog)

View File

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

View File

@@ -43,7 +43,7 @@ from psutil import virtual_memory, disk_usage
from html import escape as hescape
import pymupdf
from .shared import IMAGE_TYPES, getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .shared import getImageFileName, walkSort, walkLevel, sanitizeTrace, subprocess_run, dot_clean
from .comicarchive import SEVENZIP, available_archive_tools
from . import comic2panel
from . import image
@@ -456,7 +456,7 @@ def buildOPF(dstdir, title, filelist, originalpath, cover=None):
"</container>"])
f.close()
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, job_progress='', len_tomes=0):
def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, originalpath, len_tomes=0):
filelist = []
chapterlist = []
os.mkdir(os.path.join(path, 'OEBPS', 'Text'))
@@ -564,7 +564,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
else:
filelist.append(buildHTML(dirpath, afile, os.path.join(dirpath, afile)))
build_html_end = perf_counter()
print(f"{job_progress}buildHTML: {build_html_end - build_html_start} seconds")
print(f"buildHTML: {build_html_end - build_html_start} seconds")
# Overwrite chapternames if ComicInfo.xml has bookmarks
if ischunked:
options.comicinfo_chapters = []
@@ -600,7 +600,7 @@ def buildEPUB(path, chapternames, tomenumber, ischunked, cover: image.Cover, ori
buildOPF(path, options.title, filelist, originalpath, cover)
def buildPDF(path, title, job_progress='', cover=None, output_file=None):
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.
@@ -625,11 +625,11 @@ def buildPDF(path, title, job_progress='', cover=None, output_file=None):
# Save with optimizations for smaller file size
doc.save(output_file, deflate=True, garbage=4, clean=True)
end = perf_counter()
print(f"{job_progress}MuPDF output: {end-start} sec")
print(f"MuPDF output: {end-start} sec")
return output_file
def imgDirectoryProcessing(path, job_progress=''):
def imgDirectoryProcessing(path):
global workerPool, workerOutput
workerPool = Pool(maxtasksperchild=100)
workerOutput = []
@@ -649,7 +649,7 @@ def imgDirectoryProcessing(path, job_progress=''):
workerPool.close()
workerPool.join()
img_processing_end = perf_counter()
print(f"{job_progress}imgFileProcessing: {img_processing_end - img_processing_start} seconds")
print(f"imgFileProcessing: {img_processing_end - img_processing_start} seconds")
# macOS 15 likes to add ._ files after multiprocessing
dot_clean(path)
@@ -870,9 +870,6 @@ def getWorkFolder(afile):
raise UserWarning("Not enough disk space to perform conversion.")
if afile.lower().endswith('.pdf'):
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
if not os.path.exists(fullPath):
os.makedirs(fullPath)
path = workdir
sanitizePermissions(path)
target_height = options.profileData[1][1]
@@ -881,44 +878,39 @@ def getWorkFolder(afile):
elif options.cropping == 2:
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_height)
mupdf_pdf_process_pages_parallel(afile, workdir, target_height)
except Exception as e:
rmtree(path, True)
raise UserWarning(f"Failed to extract images from PDF file. {e}")
return workdir
else:
workdir = mkdtemp('', 'KCC-', os.path.dirname(afile))
fullPath = os.path.join(workdir, 'OEBPS', 'Images')
if not os.path.exists(fullPath):
os.makedirs(fullPath)
try:
cbx = comicarchive.ComicArchive(afile)
path = cbx.extract(fullPath)
path = cbx.extract(workdir)
sanitizePermissions(path)
tdir = os.listdir(fullPath)
tdir = os.listdir(workdir)
if len(tdir) == 2 and 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if os.path.isdir(os.path.join(fullPath, tdir[0])):
if os.path.isdir(os.path.join(workdir, tdir[0])):
os.replace(
os.path.join(fullPath, 'ComicInfo.xml'),
os.path.join(fullPath, tdir[0], 'ComicInfo.xml')
os.path.join(workdir, 'ComicInfo.xml'),
os.path.join(workdir, tdir[0], 'ComicInfo.xml')
)
if len(tdir) == 1 and os.path.isdir(os.path.join(fullPath, tdir[0])):
for file in os.listdir(os.path.join(fullPath, tdir[0])):
move(os.path.join(fullPath, tdir[0], file), fullPath)
os.rmdir(os.path.join(fullPath, tdir[0]))
return workdir
if len(tdir) == 1 and os.path.isdir(os.path.join(workdir, tdir[0])):
path = os.path.join(workdir, tdir[0])
except OSError as e:
rmtree(workdir, True)
raise UserWarning(e)
else:
raise UserWarning("Failed to open source file/directory.")
newpath = mkdtemp('', 'KCC-', os.path.dirname(afile))
os.renames(path, os.path.join(newpath, 'OEBPS', 'Images'))
return newpath
def getOutputFilename(srcpath, wantedname, ext, tomenumber):
source_path = Path(srcpath)
if srcpath[-1] == os.path.sep:
srcpath = srcpath[:-1]
if 'Ko' in options.profile and options.format == 'EPUB':
@@ -928,29 +920,20 @@ def getOutputFilename(srcpath, wantedname, ext, tomenumber):
else:
ext = '.kepub.epub'
if wantedname is not None:
wanted_root, wanted_ext = os.path.splitext(wantedname)
if wantedname.endswith(ext):
filename = os.path.abspath(wantedname)
elif wanted_ext == '.mobi' and ext == '.epub':
filename = os.path.abspath(wanted_root + ext)
# output directory
elif os.path.isdir(srcpath):
filename = os.path.join(os.path.abspath(options.output), os.path.basename(srcpath) + ext)
else:
abs_path = os.path.abspath(options.output)
if not os.path.exists(abs_path):
os.mkdir(abs_path)
if source_path.is_file():
filename = os.path.join(os.path.abspath(options.output), source_path.stem + tomenumber + ext)
else:
filename = os.path.join(os.path.abspath(options.output), source_path.name + tomenumber + ext)
filename = os.path.join(os.path.abspath(options.output),
os.path.basename(os.path.splitext(srcpath)[0]) + ext)
elif os.path.isdir(srcpath):
filename = srcpath + tomenumber + ext
else:
if 'Ko' in options.profile and options.format == 'EPUB':
if source_path.is_file():
name = re.sub(r'\W+', '_', source_path.stem) + tomenumber + ext
else:
name = re.sub(r'\W+', '_', source_path.name) + tomenumber + ext
filename = source_path.with_name(name)
src = pathlib.Path(srcpath)
name = re.sub(r'\W+', '_', src.stem) + tomenumber + ext
filename = src.with_name(name)
else:
filename = os.path.splitext(srcpath)[0] + tomenumber + ext
if os.path.isfile(filename):
@@ -1056,7 +1039,7 @@ def removeNonImages(filetree):
for root, dirs, files in os.walk(filetree):
for name in files:
_, ext = getImageFileName(name)
if ext not in IMAGE_TYPES:
if ext not in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif'):
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
# remove empty nested folders
@@ -1266,7 +1249,7 @@ 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))
return value
def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
def makeZIP(zipfilename, basedir, isepub=False):
start = perf_counter()
zipfilename = os.path.abspath(zipfilename) + '.zip'
if SEVENZIP in available_archive_tools():
@@ -1287,7 +1270,7 @@ def makeZIP(zipfilename, basedir, job_progress='', isepub=False):
zipOutput.write(path, aPath)
zipOutput.close()
end = perf_counter()
print(f"{job_progress}makeZIP time: {end - start} seconds")
print(f"makeZIP time: {end - start} seconds")
return zipfilename
def makeParser():
@@ -1551,7 +1534,7 @@ def makeFusion(sources: List[str]):
return str(fusion_path)
def makeBook(source, qtgui=None, job_progress=''):
def makeBook(source, qtgui=None):
start = perf_counter()
global GUI
GUI = qtgui
@@ -1559,12 +1542,11 @@ def makeBook(source, qtgui=None, job_progress=''):
GUI.progressBarTick.emit('1')
else:
checkTools(source)
options.kindle_azw3 = options.iskindle and ('MOBI' in options.format or 'EPUB' in options.format)
options.kindle_scribe_azw3 = options.profile == 'KS' and ('MOBI' in options.format or 'EPUB' in options.format)
checkPre(source)
print(f"{job_progress}Preparing source images...")
print("Preparing source images...")
path = getWorkFolder(source)
print(f"{job_progress}Checking images...")
print("Checking images...")
getMetadata(os.path.join(path, "OEBPS", "Images"), source)
removeNonImages(os.path.join(path, "OEBPS", "Images"))
detectSuboptimalProcessing(os.path.join(path, "OEBPS", "Images"), source)
@@ -1575,14 +1557,14 @@ def makeBook(source, qtgui=None, job_progress=''):
if options.webtoon:
x, y = image.ProfileData.Profiles[options.profile][1]
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], job_progress, qtgui)
comic2panel.main(['-y ' + str(y), '-x' + str(x), '-i', '-m', path], qtgui)
if options.noprocessing:
print(f"{job_progress}Do not process image, ignore any profile or processing option")
print("Do not process image, ignore any profile or processing option")
else:
print(f"{job_progress}Processing images...")
print("Processing images...")
if GUI:
GUI.progressBarTick.emit(f'{job_progress}Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"), job_progress)
GUI.progressBarTick.emit('Processing images')
imgDirectoryProcessing(os.path.join(path, "OEBPS", "Images"))
if GUI:
GUI.progressBarTick.emit('1')
if options.batchsplit > 0 or options.targetsize:
@@ -1593,11 +1575,11 @@ def makeBook(source, qtgui=None, job_progress=''):
tomeNumber = 0
if GUI:
if options.format == 'CBZ':
GUI.progressBarTick.emit(f'{job_progress}Compressing CBZ files')
GUI.progressBarTick.emit('Compressing CBZ files')
elif options.format == 'PDF':
GUI.progressBarTick.emit(f'{job_progress}Creating PDF files')
GUI.progressBarTick.emit('Creating PDF files')
else:
GUI.progressBarTick.emit(f'{job_progress}Compressing EPUB files')
GUI.progressBarTick.emit('Compressing EPUB files')
GUI.progressBarTick.emit(str(len(tomes) + 1))
GUI.progressBarTick.emit('tick')
options.baseTitle = options.title
@@ -1611,29 +1593,29 @@ def makeBook(source, qtgui=None, job_progress=''):
tomeNumber += 1
options.title = options.baseTitle + ' [' + str(tomeNumber) + '/' + str(len(tomes)) + ']'
if options.format == 'CBZ':
print(f"{job_progress}Creating CBZ file...")
print("Creating CBZ file...")
if len(tomes) > 1:
filepath.append(getOutputFilename(source, options.output, '.cbz', ' ' + str(tomeNumber)))
else:
filepath.append(getOutputFilename(source, options.output, '.cbz', ''))
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"), job_progress)
makeZIP(tome + '_comic', os.path.join(tome, "OEBPS", "Images"))
elif options.format == 'PDF':
print(f"{job_progress}Creating PDF file with PyMuPDF...")
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, job_progress, None, output_file)
output_pdf = buildPDF(tome, options.title, None, output_file)
filepath.append(output_pdf)
else:
print(f"{job_progress}Creating EPUB file...")
print("Creating EPUB file...")
if len(tomes) > 1:
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, job_progress, len(tomes))
buildEPUB(tome, chapterNames, tomeNumber, True, cover, source, len(tomes))
filepath.append(getOutputFilename(source, options.output, '.epub', ' ' + str(tomeNumber)))
else:
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source, job_progress)
buildEPUB(tome, chapterNames, tomeNumber, False, cover, source)
filepath.append(getOutputFilename(source, options.output, '.epub', ''))
makeZIP(tome + '_comic', tome, job_progress, 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])
@@ -1646,23 +1628,23 @@ def makeBook(source, qtgui=None, job_progress=''):
if GUI:
GUI.progressBarTick.emit('tick')
if not GUI and options.format == 'MOBI':
print(f"{job_progress}Creating MOBI files...")
print("Creating MOBI files...")
work = []
for i in filepath:
work.append([i])
output = makeMOBI(work, GUI)
for errors in output:
if errors[0] != 0:
print(f"{job_progress}Error: KindleGen failed to create MOBI!")
print('Error: KindleGen failed to create MOBI!')
print(errors)
return filepath
k = kindle.Kindle(options.profile)
if k.path and k.coverSupport:
print(f"{job_progress}Kindle detected. Uploading covers...")
print("Kindle detected. Uploading covers...")
for i in filepath:
output = makeMOBIFix(i, options.covers[filepath.index(i)][1])
if not output[0]:
print(f'{job_progress}Error: Failed to tweak KindleGen output!')
print('Error: Failed to tweak KindleGen output!')
return filepath
else:
os.remove(i.replace('.epub', '.mobi') + '_toclean')
@@ -1675,7 +1657,7 @@ def makeBook(source, qtgui=None, job_progress=''):
rmtree(source, True)
end = perf_counter()
print(f"{job_progress}makeBook: {end - start} seconds")
print(f"makeBook: {end - start} seconds")
# Clean up temporary workspace
try:
rmtree(path, True)

View File

@@ -221,7 +221,7 @@ def splitImage(work):
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
def main(argv=None, job_progress='', qtgui=None):
def main(argv=None, qtgui=None):
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
@@ -262,7 +262,7 @@ def main(argv=None, job_progress='', qtgui=None):
splitWorkerOutput = []
splitWorkerPool = Pool(maxtasksperchild=10)
if args.merge:
print(f"{job_progress}Merging images...")
print("Merging images...")
directoryNumer = 1
mergeWork = []
mergeWorkerOutput = []
@@ -274,7 +274,7 @@ def main(argv=None, job_progress='', qtgui=None):
directoryNumer += 1
mergeWork.append([os.path.join(root, directory)])
if GUI:
GUI.progressBarTick.emit(f'{job_progress}Combining images')
GUI.progressBarTick.emit('Combining images')
GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
@@ -287,7 +287,7 @@ def main(argv=None, job_progress='', qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1])
print(f"{job_progress}Splitting images...")
print("Splitting images...")
dot_clean(targetDir)
for root, _, files in os.walk(targetDir, False):
for name in files:
@@ -297,7 +297,7 @@ def main(argv=None, job_progress='', qtgui=None):
else:
os.remove(os.path.join(root, name))
if GUI:
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
GUI.progressBarTick.emit('Splitting images')
GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick')
if len(work) > 0:

View File

@@ -20,13 +20,12 @@
from functools import cached_property, lru_cache
import os
from pathlib import Path
import platform
import distro
from subprocess import STDOUT, PIPE, CalledProcessError
from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError
from .shared import IMAGE_TYPES, subprocess_run
from .shared import subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
@@ -66,9 +65,6 @@ class ComicArchive:
def extract(self, targetdir):
if not os.path.isdir(targetdir):
raise OSError('Target directory doesn\'t exist.')
if Path(self.basename).suffix.lower() in IMAGE_TYPES:
raise UserWarning('Put images into folder and drag and drop folder into KCC window.')
missing = []

View File

@@ -86,9 +86,6 @@ class ProfileData:
]
ProfilesKindleEBOK = {
}
ProfilesKindlePDOC = {
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
@@ -96,6 +93,9 @@ class ProfileData:
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
}
ProfilesKindlePDOC = {
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
@@ -263,6 +263,7 @@ class ComicPage:
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.kindle_scribe_azw3 = (options.profile == 'KS') and (options.format in ('MOBI', 'EPUB'))
self.original_color_mode = image.mode
# TODO: color check earlier
self.image = image.convert("RGB")
@@ -480,14 +481,6 @@ class ComicPage:
self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self):
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
# TODO: Kindle Scribe case
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
elif self.image.size[0] > self.size[0] * 2 or self.image.size[1] > self.size[1]:
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1], Image.Resampling.LANCZOS))
return
ratio_device = float(self.size[1]) / float(self.size[0])
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()

View File

@@ -27,9 +27,6 @@ import sys
from traceback import format_tb
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
class HTMLStripper(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)

View File

@@ -6,6 +6,5 @@ packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
# Below requirements are compiled in Dockefile
# numpy==2.3.4
# PyMuPDF==1.26.6
numpy>=1.22.4
PyMuPDF>=1.18.0

View File

@@ -1,4 +1,4 @@
PySide6==6.4.3
PySide6==6.5.2
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0

View File

@@ -2,7 +2,7 @@ PySide6<6.10
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1,<9.0.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0

View File

@@ -148,15 +148,16 @@ setuptools.setup(
},
packages=['kindlecomicconverter'],
install_requires=[
'PySide6>=6.0.0',
'pyside6>=6.0.0',
'Pillow>=9.3.0',
'PyMuPDF>=1.18.0',
'psutil>=5.9.5',
'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'mozjpeg-lossless-optimization>=1.2.0',
'requests>=2.31.0',
'mozjpeg-lossless-optimization>=1.1.2',
'natsort>=8.4.0',
'distro>=1.8.0',
'distro',
'numpy>=1.22.4',
'PyMuPDF>=1.16.1',
],