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

Compare commits

..

5 Commits

Author SHA1 Message Date
darodi
c98d6179c3 docker update (#521) 2023-05-14 17:10:40 +00:00
Alex Xu
37200bdca0 Crop images within 3% of device aspect ratio (#495)
* Crop images within 3% of device aspect ratio

* use pad instead of expand+fit

* reafactor threshold check

* remove default val
2023-05-14 16:34:11 +00:00
Jaroslaw Janas
0193bcd00a conda environment (#520) 2023-05-14 16:32:06 +00:00
darodi
0bbe9348a2 OptionParser to ArgumentParser (#517) 2023-05-14 16:31:45 +00:00
darodi
d16628dc59 last version check (#518) 2023-05-14 16:31:31 +00:00
15 changed files with 507 additions and 482 deletions

View File

@@ -0,0 +1,34 @@
name: Docker base
on:
workflow_dispatch:
push:
tags: [ 'docker-base-*' ]
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- 'docs/**'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build_and_push:
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
with:
docker_build_file: ./Dockerfile-base
platform_linux_arm32v7_enabled: true
platform_linux_arm64v8_enabled: true
platform_linux_amd64_enabled: true
push_enabled: true
build_nohealthcheck: false
ghcr_repo_owner: ${{ github.repository_owner }}
ghcr_repo: ${{ github.repository }}
build_latest: false
secrets:
ghcr_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,10 +2,7 @@ name: Docker
on: on:
workflow_dispatch: workflow_dispatch:
#schedule:
# - cron: '39 5 * * *'
push: push:
# branches: [ master, pipeline_test, docker_test ]
# Publish semver tags as releases. # Publish semver tags as releases.
tags: [ 'v*.*.*' ] tags: [ 'v*.*.*' ]

View File

@@ -34,7 +34,7 @@ jobs:
- name: Install python dependencies - name: Install python dependencies
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full python3-pyqt5 python3-pip squashfs-tools libfuse2 sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pyqt5 python3-pip squashfs-tools libfuse2
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller PyQt6 --no-binary pyinstaller python -m pip install --upgrade pip setuptools wheel certifi pyinstaller PyQt6 --no-binary pyinstaller
python -m pip install -r requirements.txt python -m pip install -r requirements.txt
- name: build binary - name: build binary

View File

@@ -1,147 +1,5 @@
FROM --platform=linux/amd64 python:3.11-slim-buster as compile-amd64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 python3-pyqt5 && \
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install -r /opt/kcc/requirements.txt
######################################################################################
FROM --platform=linux/arm64 python:3.11-slim-buster as compile-arm64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(python-pyqt5) && \
KEPT_PACKAGES+=(qt5-default) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
######################################################################################
FROM --platform=linux/arm/v7 python:3.11-slim-buster as compile-armv7
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libffi-dev) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(python-pyqt5) && \
KEPT_PACKAGES+=(qt5-default) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
######################################################################################
FROM --platform=linux/amd64 python:3.11-slim-buster as build-amd64
COPY --from=compile-amd64 /opt/venv /opt/venv
FROM --platform=linux/arm64 python:3.11-slim-buster as build-arm64
COPY --from=compile-arm64 /opt/venv /opt/venv
FROM --platform=linux/arm/v7 python:3.11-slim-buster as build-armv7
COPY --from=compile-armv7 /opt/venv /opt/venv
######################################################################################
# Select final stage based on TARGETARCH ARG # Select final stage based on TARGETARCH ARG
FROM build-${TARGETARCH}${TARGETVARIANT} FROM ghcr.io/ciromattia/kcc:docker-base-20230514
LABEL com.kcc.name="Kindle Comic Converter" LABEL com.kcc.name="Kindle Comic Converter"
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi" LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
LABEL org.opencontainers.image.description='Kindle Comic Converter' LABEL org.opencontainers.image.description='Kindle Comic Converter'
@@ -154,14 +12,8 @@ LABEL org.opencontainers.image.vendor='ciromattia'
LABEL org.opencontainers.image.licenses='ISC' LABEL org.opencontainers.image.licenses='ISC'
LABEL org.opencontainers.image.title="Kindle Comic Converter" LABEL org.opencontainers.image.title="Kindle Comic Converter"
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . /opt/kcc COPY . /opt/kcc
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \ RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
apt-get install -y p7zip-full unrar-free && \
ln -s /app/kindlegen /bin/kindlegen && \
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"] ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
CMD ["-h"] CMD ["-h"]

162
Dockerfile-base Normal file
View File

@@ -0,0 +1,162 @@
FROM --platform=linux/amd64 python:3.11-slim-bullseye as compile-amd64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
COPY requirements.txt /opt/kcc/
ENV PATH="/opt/venv/bin:$PATH"
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y libpng-dev libjpeg-dev p7zip-full unrar-free libgl1 python3-pyqt5 && \
python -m pip install --upgrade pip && \
python -m venv /opt/venv && \
python -m pip install -r /opt/kcc/requirements.txt
######################################################################################
FROM --platform=linux/arm64 python:3.11-slim-bullseye as compile-arm64
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(python3-pyqt5) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
######################################################################################
FROM --platform=linux/arm/v7 python:3.11-slim-bullseye as compile-armv7
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
RUN echo "I'm building for $TARGETOS/$TARGETARCH/$TARGETVARIANT"
ENV LC_ALL=C.UTF-8 \
LANG=C.UTF-8 \
LANGUAGE=en_US:en
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN set -x && \
TEMP_PACKAGES=() && \
KEPT_PACKAGES=() && \
# Packages only required during build
TEMP_PACKAGES+=(build-essential) && \
TEMP_PACKAGES+=(cmake) && \
TEMP_PACKAGES+=(libffi-dev) && \
TEMP_PACKAGES+=(libfreetype6-dev) && \
TEMP_PACKAGES+=(libfontconfig1-dev) && \
TEMP_PACKAGES+=(libpng-dev) && \
TEMP_PACKAGES+=(libjpeg-dev) && \
TEMP_PACKAGES+=(libssl-dev) && \
TEMP_PACKAGES+=(libxft-dev) && \
TEMP_PACKAGES+=(make) && \
TEMP_PACKAGES+=(python3-dev) && \
TEMP_PACKAGES+=(python3-setuptools) && \
TEMP_PACKAGES+=(python3-wheel) && \
# Packages kept in the image
KEPT_PACKAGES+=(bash) && \
KEPT_PACKAGES+=(ca-certificates) && \
KEPT_PACKAGES+=(chrpath) && \
KEPT_PACKAGES+=(locales) && \
KEPT_PACKAGES+=(locales-all) && \
KEPT_PACKAGES+=(libfreetype6) && \
KEPT_PACKAGES+=(libfontconfig1) && \
KEPT_PACKAGES+=(p7zip-full) && \
KEPT_PACKAGES+=(python3) && \
KEPT_PACKAGES+=(python3-pip) && \
KEPT_PACKAGES+=(python3-pyqt5) && \
KEPT_PACKAGES+=(unrar-free) && \
# Install packages
DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
${KEPT_PACKAGES[@]} \
${TEMP_PACKAGES[@]} \
&& \
# Install required python modules
python -m pip install --upgrade pip && \
# python -m pip install -r /opt/kcc/requirements.txt && \
python -m venv /opt/venv && \
python -m pip install --upgrade pillow python-slugify psutil raven mozjpeg-lossless-optimization
######################################################################################
FROM --platform=linux/amd64 python:3.11-slim-bullseye as build-amd64
COPY --from=compile-amd64 /opt/venv /opt/venv
FROM --platform=linux/arm64 python:3.11-slim-bullseye as build-arm64
COPY --from=compile-arm64 /opt/venv /opt/venv
FROM --platform=linux/arm/v7 python:3.11-slim-bullseye as build-armv7
COPY --from=compile-armv7 /opt/venv /opt/venv
######################################################################################
# Select final stage based on TARGETARCH ARG
FROM build-${TARGETARCH}${TARGETVARIANT}
LABEL com.kcc.name="Kindle Comic Converter base image"
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
LABEL org.opencontainers.image.description='Kindle Comic Converter base image'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.source='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.authors='darodi'
LABEL org.opencontainers.image.url='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.documentation='https://github.com/ciromattia/kcc'
LABEL org.opencontainers.image.vendor='ciromattia'
LABEL org.opencontainers.image.licenses='ISC'
LABEL org.opencontainers.image.title="Kindle Comic Converter"
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
RUN DEBIAN_FRONTEND=noninteractive apt-get update -y && apt-get -yq upgrade && \
apt-get install -y p7zip-full unrar-free && \
ln -s /app/kindlegen /bin/kindlegen && \
echo docker-base-20230514 > /IMAGE_VERSION

124
README.md
View File

@@ -136,89 +136,81 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
### Standalone `kcc-c2e.py` usage: ### Standalone `kcc-c2e.py` usage:
``` ```
Usage: kcc-c2e [options] comic_file|comic_folder usage: kcc-c2e [options] [input]
Options: MANDATORY:
MAIN: input Full path to comic folder or file(s) to be processed.
-p PROFILE, --profile=PROFILE
Device profile (Available options: K1, K2, K34, K578,
KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD,
KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoL, KoF, KoS,
KoE) [Default=KV]
-m, --manga-style Manga style (right-to-left reading and splitting)
-q, --hq Try to increase the quality of magnification
-2, --two-panel Display two not four panels in Panel View mode
-w, --webtoon Webtoon processing mode
--targetsize=TARGETSIZE
the maximal size of output file in MB. [Default=100MB
for webtoon and 400MB for others]
OUTPUT SETTINGS: MAIN:
-o OUTPUT, --output=OUTPUT -p PROFILE, --profile PROFILE
Output generated file to specified directory or file Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoL, KoF, KoS, KoE) [Default=KV]
-t TITLE, --title=TITLE -m, --manga-style Manga style (right-to-left reading and splitting)
Comic title [Default=filename or directory name] -q, --hq Try to increase the quality of magnification
-f FORMAT, --format=FORMAT -2, --two-panel Display two not four panels in Panel View mode
Output format (Available options: Auto, MOBI, EPUB, -w, --webtoon Webtoon processing mode
CBZ, KFX, MOBI+EPUB) [Default=Auto] --ts TARGETSIZE, --targetsize TARGETSIZE
-b BATCHSPLIT, --batchsplit=BATCHSPLIT the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
Split output into multiple files. 0: Don't split 1:
Automatic mode 2: Consider every subdirectory as
separate volume [Default=0]
PROCESSING: PROCESSING:
-n, --noprocessing Do not modify image and ignore any profil or -n, --noprocessing Do not modify image and ignore any profil or processing option
processing option -u, --upscale Resize images smaller than device's resolution
-u, --upscale Resize images smaller than device's resolution -s, --stretch Stretch images to device's resolution
-s, --stretch Stretch images to device's resolution -r SPLITTER, --splitter SPLITTER
-r SPLITTER, --splitter=SPLITTER Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
Double page parsing mode. 0: Split 1: Rotate 2: Both -g GAMMA, --gamma GAMMA
[Default=0] Apply gamma correction to linearize the image [Default=Auto]
-g GAMMA, --gamma=GAMMA -c CROPPING, --cropping CROPPING
Apply gamma correction to linearize the image Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]
[Default=Auto] --cp CROPPINGP, --croppingpower CROPPINGP
-c CROPPING, --cropping=CROPPING
Set cropping mode. 0: Disabled 1: Margins 2: Margins +
page numbers [Default=2]
--cp=CROPPINGP, --croppingpower=CROPPINGP
Set cropping power [Default=1.0] Set cropping power [Default=1.0]
--cm=CROPPINGM, --croppingminimum=CROPPINGM --cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0] Set cropping minimum area ratio [Default=0.0]
--blackborders Disable autodetection and force black borders --blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders --whiteborders Disable autodetection and force white borders
--forcecolor Don't convert images to grayscale --forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG --forcepng Create PNG files instead JPEG
--mozjpeg Create JPEG files using mozJpeg --mozjpeg Create JPEG files using mozJpeg
--maximizestrips Turn 1x4 strips to 2x2 strips --maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not -d, --delete Delete source file(s) or a directory. It's not recoverable.
recoverable.
CUSTOM PROFILE: OUTPUT SETTINGS:
--customwidth=CUSTOMWIDTH -o OUTPUT, --output OUTPUT
Output generated file to specified directory or file
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
-f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) [Default=Auto]
-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]
CUSTOM PROFILE:
--customwidth CUSTOMWIDTH
Replace screen width provided by device profile Replace screen width provided by device profile
--customheight=CUSTOMHEIGHT --customheight CUSTOMHEIGHT
Replace screen height provided by device profile Replace screen height provided by device profile
OTHER: OTHER:
-h, --help Show this help message and exit -h, --help Show this help message and exit
``` ```
### Standalone `kcc-c2p.py` usage: ### Standalone `kcc-c2p.py` usage:
``` ```
Usage: kcc-c2p [options] comic_folder usage: kcc-c2p [options] [input]
Options: MANDATORY:
MANDATORY: input Full path to comic folder(s) to be processed. Separate multiple inputs with spaces.
-y HEIGHT, --height=HEIGHT
MAIN:
-y HEIGHT, --height HEIGHT
Height of the target device screen Height of the target device screen
-i, --in-place Overwrite source directory -i, --in-place Overwrite source directory
-m, --merge Combine every directory into a single image before -m, --merge Combine every directory into a single image before splitting
splitting
OTHER: OTHER:
-d, --debug Create debug file for every split image -d, --debug Create debug file for every split image
-h, --help Show this help message and exit -h, --help Show this help message and exit
``` ```
## CREDITS ## CREDITS

15
environment.yml Normal file
View File

@@ -0,0 +1,15 @@
name: kcc
channels:
- conda-forge
- defaults
dependencies:
- python=3.8
- Pillow>=5.2.0
- psutil>=5.0.0
- python-slugify>=1.2.1
- raven>=6.0.0
- distro
- pip
- pip:
- mozjpeg-lossless-optimization>=1.1.2
- PyQt5>=5.6.0

View File

@@ -19,8 +19,9 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import sys import sys
if sys.version_info[0] != 3:
print('ERROR: This is Python 3 script!') if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
exit(1) exit(1)
from multiprocessing import freeze_support, set_start_method from multiprocessing import freeze_support, set_start_method

View File

@@ -19,8 +19,9 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import sys import sys
if sys.version_info[0] != 3:
print('ERROR: This is Python 3 script!') if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
exit(1) exit(1)
from multiprocessing import freeze_support, set_start_method from multiprocessing import freeze_support, set_start_method

5
kcc.py
View File

@@ -19,8 +19,9 @@
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import sys import sys
if sys.version_info[0] != 3:
print('ERROR: This is Python 3 script!') if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
exit(1) exit(1)
# OS specific workarounds # OS specific workarounds

View File

@@ -16,11 +16,12 @@
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER # OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE. # PERFORMANCE OF THIS SOFTWARE.
import json
import os import os
import re
import sys import sys
from urllib.parse import unquote from urllib.parse import unquote
from urllib.request import urlretrieve from urllib.request import urlretrieve, urlopen
from time import sleep from time import sleep
from shutil import move, rmtree from shutil import move, rmtree
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
@@ -142,64 +143,27 @@ class VersionThread(QtCore.QThread):
self.wait() self.wait()
def run(self): def run(self):
# TODO adapt with github releases try:
pass last_version_url = urlopen("https://api.github.com/repos/ciromattia/kcc/releases/latest")
data = last_version_url.read()
encoding = last_version_url.info().get_content_charset('utf-8')
json_parser = json.loads(data.decode(encoding))
# try: html_url = json_parser["html_url"]
# XML = parse(urlopen(Request('https://kcc.iosphe.re/Version/', latest_version = json_parser["tag_name"]
# headers={'User-Agent': 'KindleComicConverter/' + __version__}))) latest_version = re.sub(r'^v', "", latest_version)
# except Exception:
# return if ("b" not in __version__ and StrictVersion(latest_version) > StrictVersion(__version__)) \
# latestVersion = XML.childNodes[0].getElementsByTagName('LatestVersion')[0].childNodes[0].toxml() or ("b" in __version__
# if ("beta" not in __version__ and StrictVersion(latestVersion) > StrictVersion(__version__)) \ and StrictVersion(latest_version) >= StrictVersion(re.sub(r'b.*', '', __version__))):
# or ("beta" in __version__ MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
# and StrictVersion(latestVersion) >= StrictVersion(re.sub(r'-beta.*', '', __version__))): False)
# if sys.platform.startswith('win'): except Exception:
# self.newVersion = latestVersion return
# self.md5 = XML.childNodes[0].getElementsByTagName('MD5')[0].childNodes[0].toxml()
# MW.showDialog.emit('<b>New version released!</b> <a href="https://github.com/ciromattia/kcc/releases/">'
# 'See changelog.</a><br/><br/>Installed version: ' + __version__ +
# '<br/>Current version: ' + latestVersion +
# '<br/><br/>Would you like to start automatic update?', 'question')
# self.getNewVersion()
# else:
# MW.addMessage.emit('<a href="https://kcc.iosphe.re/">'
# '<b>The new version is available!</b></a> '
# '(<a href="https://github.com/ciromattia/kcc/releases/">'
# 'Changelog</a>)', 'warning', False)
def setAnswer(self, dialoganswer): def setAnswer(self, dialoganswer):
self.answer = dialoganswer self.answer = dialoganswer
def getNewVersion(self):
while self.answer is None:
sleep(1)
if self.answer == QtWidgets.QMessageBox.Yes:
try:
MW.modeConvert.emit(-1)
MW.progressBarTick.emit('Downloading update')
path = urlretrieve('https://kcc.iosphe.re/Windows/KindleComicConverter_win_' +
self.newVersion + '.exe', reporthook=self.getNewVersionTick)
if self.md5 != md5Checksum(path[0]):
raise Exception
move(path[0], path[0] + '.exe')
MW.hideProgressBar.emit()
MW.modeConvert.emit(1)
Popen(path[0] + '.exe /SP- /silent /noicons', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
MW.forceShutdown.emit()
except Exception:
MW.addMessage.emit('Failed to download the update!', 'warning', False)
MW.hideProgressBar.emit()
MW.modeConvert.emit(1)
def getNewVersionTick(self, size, blocksize, totalsize):
progress = int((size / (totalsize // blocksize)) * 100)
if size == 0:
MW.progressBarTick.emit('100')
if progress > self.barProgress:
self.barProgress = progress
MW.progressBarTick.emit('tick')
class ProgressThread(QtCore.QThread): class ProgressThread(QtCore.QThread):
def __init__(self): def __init__(self):
@@ -255,7 +219,7 @@ class WorkerThread(QtCore.QThread):
MW.modeConvert.emit(0) MW.modeConvert.emit(0)
parser = comic2ebook.makeParser() parser = comic2ebook.makeParser()
options, _ = parser.parse_args() options = parser.parse_args()
argv = '' argv = ''
currentJobs = [] currentJobs = []
@@ -506,7 +470,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)') 'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
else: else:
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath, 'Comic (*.pdf);;All (*.*)') fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
'Comic (*.pdf);;All (*.*)')
for fname in fnames[0]: for fname in fnames[0]:
if fname != '': if fname != '':
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
@@ -617,7 +582,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
def togglecroppingBox(self, value): def togglecroppingBox(self, value):
if value: if value:
GUI.croppingWidget.setVisible(True) GUI.croppingWidget.setVisible(True)
else: else:
GUI.croppingWidget.setVisible(False) GUI.croppingWidget.setVisible(False)
self.changeCroppingPower(100) # 1.0 self.changeCroppingPower(100) # 1.0
@@ -731,8 +696,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.Ok) QtWidgets.QMessageBox.critical(MW, 'KCC - Error', message, QtWidgets.QMessageBox.Ok)
elif kind == 'question': elif kind == 'question':
GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message, GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message,
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.Yes,
QtWidgets.QMessageBox.No)) QtWidgets.QMessageBox.No))
def updateProgressbar(self, command): def updateProgressbar(self, command):
if command == 'tick': if command == 'tick':

View File

@@ -20,6 +20,7 @@
import os import os
import sys import sys
from argparse import ArgumentParser
from time import strftime, gmtime from time import strftime, gmtime
from copy import copy from copy import copy
from glob import glob, escape from glob import glob, escape
@@ -28,10 +29,9 @@ from stat import S_IWRITE, S_IREAD, S_IEXEC
from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
from tempfile import mkdtemp, gettempdir, TemporaryFile from tempfile import mkdtemp, gettempdir, TemporaryFile
from shutil import move, copytree, rmtree, copyfile from shutil import move, copytree, rmtree, copyfile
from optparse import OptionParser, OptionGroup
from multiprocessing import Pool from multiprocessing import Pool
from uuid import uuid4 from uuid import uuid4
from slugify import slugify as slugifyExt from slugify import slugify as slugify_ext
from PIL import Image from PIL import Image
from subprocess import STDOUT, PIPE from subprocess import STDOUT, PIPE
from psutil import Popen, virtual_memory, disk_usage from psutil import Popen, virtual_memory, disk_usage
@@ -54,23 +54,23 @@ from . import __version__
def main(argv=None): def main(argv=None):
global options global options
parser = makeParser() parser = makeParser()
optionstemplate, args = parser.parse_args(argv) args = parser.parse_args(argv)
if len(args) == 0: options = copy(args)
if not argv or options.input == []:
parser.print_help() parser.print_help()
return 0 return 0
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
sources = set([source for arg in args for source in glob(escape(arg))]) sources = set([source for option in options.input for source in glob(escape(option))])
else: else:
sources = set(args) sources = set(options.input)
if len(sources) == 0: if len(sources) == 0:
print('No matching files found.') print('No matching files found.')
return 1 return 1
for source in sources: for source in sources:
source = source.rstrip('\\').rstrip('/') source = source.rstrip('\\').rstrip('/')
options = copy(optionstemplate) options = copy(args)
options = checkOptions(options) options = checkOptions(options)
if len(sources) > 1: print('Working on ' + source + '...')
print('Working on ' + source + '...')
makeBook(source) makeBook(source)
return 0 return 0
@@ -546,7 +546,7 @@ def imgDirectoryProcessing(path):
GUI.progressBarTick.emit(str(pagenumber)) GUI.progressBarTick.emit(str(pagenumber))
if len(work) > 0: if len(work) > 0:
for i in work: for i in work:
workerPool.apply_async(func=imgFileProcessing, args=(i, ), callback=imgFileProcessingTick) workerPool.apply_async(func=imgFileProcessing, args=(i,), callback=imgFileProcessingTick)
workerPool.close() workerPool.close()
workerPool.join() workerPool.join()
if GUI and not GUI.conversionAlive: if GUI and not GUI.conversionAlive:
@@ -910,9 +910,9 @@ def createNewTome():
def slugify(value, isdir): def slugify(value, isdir):
if isdir: if isdir:
value = slugifyExt(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.') value = slugify_ext(value, regex_pattern=r'[^-a-z0-9_\.]+').strip('.')
else: else:
value = slugifyExt(value).strip('.') value = slugify_ext(value).strip('.')
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
@@ -933,85 +933,84 @@ def makeZIP(zipfilename, basedir, isepub=False):
def makeParser(): def makeParser():
psr = OptionParser(usage="Usage: kcc-c2e [options] comic_file|comic_folder", add_help_option=False) psr = ArgumentParser(prog="kcc-c2e", usage="kcc-c2e [options] [input]", add_help=False)
mainOptions = OptionGroup(psr, "MAIN") mandatory_options = psr.add_argument_group("MANDATORY")
processingOptions = OptionGroup(psr, "PROCESSING") main_options = psr.add_argument_group("MAIN")
outputOptions = OptionGroup(psr, "OUTPUT SETTINGS") processing_options = psr.add_argument_group("PROCESSING")
customProfileOptions = OptionGroup(psr, "CUSTOM PROFILE") output_options = psr.add_argument_group("OUTPUT SETTINGS")
otherOptions = OptionGroup(psr, "OTHER") custom_profile_options = psr.add_argument_group("CUSTOM PROFILE")
other_options = psr.add_argument_group("OTHER")
mainOptions.add_option("-p", "--profile", action="store", dest="profile", default="KV", mandatory_options.add_argument("input", action="extend", nargs="*", default=None,
help="Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, " help="Full path to comic folder or file(s) to be processed.")
"K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoL, KoF, KoS, KoE)"
" [Default=KV]")
mainOptions.add_option("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Manga style (right-to-left reading and splitting)")
mainOptions.add_option("-q", "--hq", action="store_true", dest="hq", default=False,
help="Try to increase the quality of magnification")
mainOptions.add_option("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
help="Display two not four panels in Panel View mode")
mainOptions.add_option("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
help="Webtoon processing mode"),
mainOptions.add_option("--targetsize", type="int", dest="targetsize", default=None,
help="the maximal size of output file in MB."
" [Default=100MB for webtoon and 400MB for others]")
outputOptions.add_option("-o", "--output", action="store", dest="output", default=None, main_options.add_argument("-p", "--profile", action="store", dest="profile", default="KV",
help="Output generated file to specified directory or file") help="Device profile (Available options: K1, K2, K34, K578, KDX, KPW, KPW5, KV, KO, "
outputOptions.add_option("-t", "--title", action="store", dest="title", default="defaulttitle", "K11, KS, KoMT, KoG, KoGHD, KoA, KoAHD, KoAH2O, KoAO, KoN, KoC, KoL, KoF, KoS, KoE)"
help="Comic title [Default=filename or directory name]") " [Default=KV]")
outputOptions.add_option("-f", "--format", action="store", dest="format", default="Auto", main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) " help="Manga style (right-to-left reading and splitting)")
"[Default=Auto]") main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
outputOptions.add_option("-b", "--batchsplit", type="int", dest="batchsplit", default="0", help="Try to increase the quality of magnification")
help="Split output into multiple files. 0: Don't split 1: Automatic mode " main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
"2: Consider every subdirectory as separate volume [Default=0]") help="Display two not four panels in Panel View mode")
main_options.add_argument("-w", "--webtoon", action="store_true", dest="webtoon", default=False,
help="Webtoon processing mode"),
main_options.add_argument("--ts", "--targetsize", type=int, dest="targetsize", default=None,
help="the maximal size of output file in MB."
" [Default=100MB for webtoon and 400MB for others]")
processingOptions.add_option("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False, output_options.add_argument("-o", "--output", action="store", dest="output", default=None,
help="Do not modify image and ignore any profil or processing option") help="Output generated file to specified directory or file")
processingOptions.add_option("-u", "--upscale", action="store_true", dest="upscale", default=False, output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
help="Resize images smaller than device's resolution") help="Comic title [Default=filename or directory name]")
processingOptions.add_option("-s", "--stretch", action="store_true", dest="stretch", default=False, output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
help="Stretch images to device's resolution") help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
processingOptions.add_option("-r", "--splitter", type="int", dest="splitter", default="0", "[Default=Auto]")
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]") output_options.add_argument("-b", "--batchsplit", type=int, dest="batchsplit", default="0",
processingOptions.add_option("-g", "--gamma", type="float", dest="gamma", default="0.0", help="Split output into multiple files. 0: Don't split 1: Automatic mode "
help="Apply gamma correction to linearize the image [Default=Auto]") "2: Consider every subdirectory as separate volume [Default=0]")
processingOptions.add_option("-c", "--cropping", type="int", dest="cropping", default="2",
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processingOptions.add_option("--cp", "--croppingpower", type="float", dest="croppingp", default="1.0",
help="Set cropping power [Default=1.0]")
processingOptions.add_option("--cm", "--croppingminimum", type="float", dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]")
processingOptions.add_option("--blackborders", action="store_true", dest="black_borders", default=False,
help="Disable autodetection and force black borders")
processingOptions.add_option("--whiteborders", action="store_true", dest="white_borders", default=False,
help="Disable autodetection and force white borders")
processingOptions.add_option("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale")
processingOptions.add_option("--forcepng", action="store_true", dest="forcepng", default=False,
help="Create PNG files instead JPEG")
processingOptions.add_option("--mozjpeg", action="store_true", dest="mozjpeg", default=False,
help="Create JPEG files using mozJpeg")
processingOptions.add_option("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
help="Turn 1x4 strips to 2x2 strips")
processingOptions.add_option("-d", "--delete", action="store_true", dest="delete", default=False,
help="Delete source file(s) or a directory. It's not recoverable.")
customProfileOptions.add_option("--customwidth", type="int", dest="customwidth", default=0, processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
help="Replace screen width provided by device profile") help="Do not modify image and ignore any profil or processing option")
customProfileOptions.add_option("--customheight", type="int", dest="customheight", default=0, processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
help="Replace screen height provided by device profile") help="Resize images smaller than device's resolution")
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
help="Stretch images to device's resolution")
processing_options.add_argument("-r", "--splitter", type=int, dest="splitter", default="0",
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
processing_options.add_argument("-g", "--gamma", type=float, dest="gamma", default="0.0",
help="Apply gamma correction to linearize the image [Default=Auto]")
processing_options.add_argument("-c", "--cropping", type=int, dest="cropping", default="2",
help="Set cropping mode. 0: Disabled 1: Margins 2: Margins + page numbers [Default=2]")
processing_options.add_argument("--cp", "--croppingpower", type=float, dest="croppingp", default="1.0",
help="Set cropping power [Default=1.0]")
processing_options.add_argument("--cm", "--croppingminimum", type=float, dest="croppingm", default="0.0",
help="Set cropping minimum area ratio [Default=0.0]")
processing_options.add_argument("--blackborders", action="store_true", dest="black_borders", default=False,
help="Disable autodetection and force black borders")
processing_options.add_argument("--whiteborders", action="store_true", dest="white_borders", default=False,
help="Disable autodetection and force white borders")
processing_options.add_argument("--forcecolor", action="store_true", dest="forcecolor", default=False,
help="Don't convert images to grayscale")
processing_options.add_argument("--forcepng", action="store_true", dest="forcepng", default=False,
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("--maximizestrips", action="store_true", dest="maximizestrips", default=False,
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.")
otherOptions.add_option("-h", "--help", action="help", custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
help="Show this help message and exit") help="Replace screen width provided by device profile")
custom_profile_options.add_argument("--customheight", type=int, dest="customheight", default=0,
help="Replace screen height provided by device profile")
other_options.add_argument("-h", "--help", action="help",
help="Show this help message and exit")
psr.add_option_group(mainOptions)
psr.add_option_group(outputOptions)
psr.add_option_group(processingOptions)
psr.add_option_group(customProfileOptions)
psr.add_option_group(otherOptions)
return psr return psr

View File

@@ -20,8 +20,8 @@
import os import os
import sys import sys
from argparse import ArgumentParser
from shutil import rmtree, copytree, move from shutil import rmtree, copytree, move
from optparse import OptionParser, OptionGroup
from multiprocessing import Pool from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw from PIL import Image, ImageChops, ImageOps, ImageDraw
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
@@ -102,7 +102,7 @@ def splitImage(work):
opt = work[2] opt = work[2]
filePath = os.path.join(path, name) filePath = os.path.join(path, name)
Image.warnings.simplefilter('error', Image.DecompressionBombWarning) Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
Image.MAX_IMAGE_PIXELS = 1000000000 Image.MAX_IMAGE_PIXELS = 1000000000
imgOrg = Image.open(filePath).convert('RGB') imgOrg = Image.open(filePath).convert('RGB')
imgProcess = Image.open(filePath).convert('1') imgProcess = Image.open(filePath).convert('1')
widthImg, heightImg = imgOrg.size widthImg, heightImg = imgOrg.size
@@ -116,7 +116,7 @@ def splitImage(work):
panelDetected = False panelDetected = False
panels = [] panels = []
while yWork < heightImg: while yWork < heightImg:
tmpImg = imgProcess.crop([4, yWork, widthImg-4, yWork + 4]) tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4))
solid = detectSolid(tmpImg) solid = detectSolid(tmpImg)
if not solid and not panelDetected: if not solid and not panelDetected:
panelDetected = True panelDetected = True
@@ -149,7 +149,7 @@ def splitImage(work):
if opt.debug: if opt.debug:
for panel in panelsProcessed: for panel in panelsProcessed:
draw.rectangle([(0, panel[0]), (widthImg, panel[1])], (0, 255, 0, 128), (0, 0, 255, 255)) draw.rectangle(((0, panel[0]), (widthImg, panel[1])), (0, 255, 0, 128), (0, 0, 255, 255))
debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg) debugImage = Image.alpha_composite(imgOrg.convert(mode='RGBA'), drawImg)
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG') debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
@@ -182,7 +182,7 @@ def splitImage(work):
if pageHeight > 15: if pageHeight > 15:
newPage = Image.new('RGB', (widthImg, pageHeight)) newPage = Image.new('RGB', (widthImg, pageHeight))
for panel in page: for panel in page:
panelImg = imgOrg.crop([0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1]]) panelImg = imgOrg.crop((0, panelsProcessed[panel][0], widthImg, panelsProcessed[panel][1]))
newPage.paste(panelImg, (0, targetHeight)) newPage.paste(panelImg, (0, targetHeight))
targetHeight += panelsProcessed[panel][2] targetHeight += panelsProcessed[panel][2]
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG') newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG')
@@ -193,97 +193,100 @@ def splitImage(work):
def main(argv=None, qtgui=None): def main(argv=None, qtgui=None):
global options, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
parser = OptionParser(usage="Usage: kcc-c2p [options] comic_folder", add_help_option=False) parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
mainOptions = OptionGroup(parser, "MANDATORY")
otherOptions = OptionGroup(parser, "OTHER") mandatory_options = parser.add_argument_group("MANDATORY")
mainOptions.add_option("-y", "--height", type="int", dest="height", default=0, main_options = parser.add_argument_group("MAIN")
help="Height of the target device screen") other_options = parser.add_argument_group("OTHER")
mainOptions.add_option("-i", "--in-place", action="store_true", dest="inPlace", default=False, mandatory_options.add_argument("input", action="extend", nargs="*", default=None,
help="Overwrite source directory") help="Full path to comic folder(s) to be processed. Separate multiple inputs"
mainOptions.add_option("-m", "--merge", action="store_true", dest="merge", default=False, " with spaces.")
help="Combine every directory into a single image before splitting") main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
otherOptions.add_option("-d", "--debug", action="store_true", dest="debug", default=False, help="Height of the target device screen")
help="Create debug file for every split image") main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
otherOptions.add_option("-h", "--help", action="help", help="Overwrite source directory")
help="Show this help message and exit") main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
parser.add_option_group(mainOptions) help="Combine every directory into a single image before splitting")
parser.add_option_group(otherOptions) other_options.add_argument("-d", "--debug", action="store_true", dest="debug", default=False,
options, args = parser.parse_args(argv) help="Create debug file for every split image")
other_options.add_argument("-h", "--help", action="help",
help="Show this help message and exit")
args = parser.parse_args(argv)
if qtgui: if qtgui:
GUI = qtgui GUI = qtgui
else: else:
GUI = None GUI = None
if len(args) != 1: if not argv or args.input == []:
parser.print_help() parser.print_help()
return 1 return 1
if options.height > 0: if args.height > 0:
options.sourceDir = args[0] for sourceDir in args.input:
options.targetDir = args[0] + "-Splitted" targetDir = sourceDir + "-Splitted"
if os.path.isdir(options.sourceDir): if os.path.isdir(sourceDir):
rmtree(options.targetDir, True) rmtree(targetDir, True)
copytree(options.sourceDir, options.targetDir) copytree(sourceDir, targetDir)
work = [] work = []
pagenumber = 1 pagenumber = 1
splitWorkerOutput = [] splitWorkerOutput = []
splitWorkerPool = Pool(maxtasksperchild=10) splitWorkerPool = Pool(maxtasksperchild=10)
if options.merge: if args.merge:
print("Merging images...") print("Merging images...")
directoryNumer = 1 directoryNumer = 1
mergeWork = [] mergeWork = []
mergeWorkerOutput = [] mergeWorkerOutput = []
mergeWorkerPool = Pool(maxtasksperchild=10) mergeWorkerPool = Pool(maxtasksperchild=10)
mergeWork.append([options.targetDir]) mergeWork.append([targetDir])
for root, dirs, files in os.walk(options.targetDir, False): for root, dirs, files in os.walk(targetDir, False):
dirs, files = walkSort(dirs, files) dirs, files = walkSort(dirs, files)
for directory in dirs: for directory in dirs:
directoryNumer += 1 directoryNumer += 1
mergeWork.append([os.path.join(root, directory)]) mergeWork.append([os.path.join(root, directory)])
if GUI:
GUI.progressBarTick.emit('Combining images')
GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
mergeWorkerPool.close()
mergeWorkerPool.join()
if GUI and not GUI.conversionAlive:
rmtree(targetDir, True)
raise UserWarning("Conversion interrupted.")
if len(mergeWorkerOutput) > 0:
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1])
print("Splitting images...")
for root, _, files in os.walk(targetDir, False):
for name in files:
if getImageFileName(name) is not None:
pagenumber += 1
work.append([root, name, args])
else:
os.remove(os.path.join(root, name))
if GUI: if GUI:
GUI.progressBarTick.emit('Combining images') GUI.progressBarTick.emit('Splitting images')
GUI.progressBarTick.emit(str(directoryNumer)) GUI.progressBarTick.emit(str(pagenumber))
for i in mergeWork: GUI.progressBarTick.emit('tick')
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick) if len(work) > 0:
mergeWorkerPool.close() for i in work:
mergeWorkerPool.join() splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
if GUI and not GUI.conversionAlive: splitWorkerPool.close()
rmtree(options.targetDir, True) splitWorkerPool.join()
raise UserWarning("Conversion interrupted.") if GUI and not GUI.conversionAlive:
if len(mergeWorkerOutput) > 0: rmtree(targetDir, True)
rmtree(options.targetDir, True) raise UserWarning("Conversion interrupted.")
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0], if len(splitWorkerOutput) > 0:
mergeWorkerOutput[0][1]) rmtree(targetDir, True)
print("Splitting images...") raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
for root, _, files in os.walk(options.targetDir, False): splitWorkerOutput[0][1])
for name in files: if args.inPlace:
if getImageFileName(name) is not None: rmtree(sourceDir)
pagenumber += 1 move(targetDir, sourceDir)
work.append([root, name, options]) else:
else: rmtree(targetDir, True)
os.remove(os.path.join(root, name)) raise UserWarning("Source directory is empty.")
if GUI:
GUI.progressBarTick.emit('Splitting images')
GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick')
if len(work) > 0:
for i in work:
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
splitWorkerPool.close()
splitWorkerPool.join()
if GUI and not GUI.conversionAlive:
rmtree(options.targetDir, True)
raise UserWarning("Conversion interrupted.")
if len(splitWorkerOutput) > 0:
rmtree(options.targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
splitWorkerOutput[0][1])
if options.inPlace:
rmtree(options.sourceDir)
move(options.targetDir, options.sourceDir)
else: else:
rmtree(options.targetDir, True) raise UserWarning("Provided input is not a directory.")
raise UserWarning("Source directory is empty.")
else:
raise UserWarning("Provided path is not a directory.")
else: else:
raise UserWarning("Target height is not set.") raise UserWarning("Target height is not set.")

View File

@@ -55,7 +55,7 @@ class ComicArchive:
def extract(self, targetdir): def extract(self, targetdir):
if not os.path.isdir(targetdir): if not os.path.isdir(targetdir):
raise OSError('Target directory don\'t exist.') raise OSError('Target directory doesn\'t exist.')
process = Popen('7z x -y -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' + targetdir + '" "' + process = Popen('7z x -y -xr!__MACOSX -xr!.DS_Store -xr!thumbs.db -xr!Thumbs.db -o"' + targetdir + '" "' +
self.filepath + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True) self.filepath + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
process.communicate() process.communicate()

View File

@@ -24,6 +24,13 @@ import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from .shared import md5Checksum from .shared import md5Checksum
# 0.045 was determined by
# 1200 / 824 = 1.456 (Kindle DX resolution)
# 2250 / 1500 = 1.5 (Typical manga page resolution)
# 1.5 - 1.456 < 0.045
# 0.045 / 1.5 = 0.03 (So maximum 3% of is cropped)
AUTO_CROP_THRESHOLD = 0.045
class ProfileData: class ProfileData:
def __init__(self): def __init__(self):
@@ -306,36 +313,32 @@ class ComicPage:
self.image = self.image.quantize(palette=palImg) self.image = self.image.quantize(palette=palImg)
def resizeImage(self): def resizeImage(self):
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]: ratio_device = float(self.size[1]) / float(self.size[0])
method = Image.Resampling.BICUBIC ratio_image = float(self.image.size[1]) / float(self.image.size[0])
else: method = self.resize_method()
method = Image.Resampling.LANCZOS
if self.opt.stretch: if self.opt.stretch:
# if self.opt.stretch or (self.opt.kfx and ('-KCC-B' in self.targetPath or '-KCC-C' in self.targetPath)):
self.image = self.image.resize(self.size, method) self.image = self.image.resize(self.size, method)
elif self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1] and not self.opt.upscale: elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
if self.opt.format == 'CBZ' or self.opt.kfx: if self.opt.format == 'CBZ' or self.opt.kfx:
borderw = int((self.size[0] - self.image.size[0]) / 2) borderw = int((self.size[0] - self.image.size[0]) / 2)
borderh = int((self.size[1] - self.image.size[1]) / 2) borderh = int((self.size[1] - self.image.size[1]) / 2)
self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill) self.image = ImageOps.expand(self.image, border=(borderw, borderh), fill=self.fill)
if self.image.size[0] != self.size[0] or self.image.size[1] != self.size[1]: if self.image.size[0] != self.size[0] or self.image.size[1] != self.size[1]:
self.image = ImageOps.fit(self.image, self.size, method=Image.Resampling.BICUBIC, centering=(0.5, 0.5)) self.image = ImageOps.fit(self.image, self.size, method=method)
else: else: # if image bigger than device resolution or smaller with upscaling
if self.opt.format == 'CBZ' or self.opt.kfx: if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
ratioDev = float(self.size[0]) / float(self.size[1]) self.image = ImageOps.fit(self.image, self.size, method=method)
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev: elif self.opt.format == 'CBZ' or self.opt.kfx:
diff = int(self.image.size[1] * ratioDev) - self.image.size[0] self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
self.image = ImageOps.expand(self.image, border=(int(diff / 2), 0), fill=self.fill)
elif (float(self.image.size[0]) / float(self.image.size[1])) > ratioDev:
diff = int(self.image.size[0] / ratioDev) - self.image.size[1]
self.image = ImageOps.expand(self.image, border=(0, int(diff / 2)), fill=self.fill)
self.image = ImageOps.fit(self.image, self.size, method=method, centering=(0.5, 0.5))
else: else:
hpercent = self.size[1] / float(self.image.size[1]) self.image = ImageOps.contain(self.image, self.size, method=method)
wsize = int((float(self.image.size[0]) * float(hpercent)))
self.image = self.image.resize((wsize, self.size[1]), method) def resize_method(self):
if self.image.size[0] > self.size[0] or self.image.size[1] > self.size[1]: if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
self.image.thumbnail(self.size, Image.Resampling.LANCZOS) method = Image.Resampling.BICUBIC
else:
method = Image.Resampling.LANCZOS
return method
def getBoundingBox(self, tmptmg): def getBoundingBox(self, tmptmg):
min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size] min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size]