mirror of
https://github.com/ciromattia/kcc
synced 2026-04-17 22:48:53 +00:00
Compare commits
5 Commits
v5.6.2
...
docker-bas
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98d6179c3 | ||
|
|
37200bdca0 | ||
|
|
0193bcd00a | ||
|
|
0bbe9348a2 | ||
|
|
d16628dc59 |
34
.github/workflows/docker-base-publish.yml
vendored
Normal file
34
.github/workflows/docker-base-publish.yml
vendored
Normal 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 }}
|
||||
3
.github/workflows/docker-publish.yml
vendored
3
.github/workflows/docker-publish.yml
vendored
@@ -2,10 +2,7 @@ name: Docker
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
#schedule:
|
||||
# - cron: '39 5 * * *'
|
||||
push:
|
||||
# branches: [ master, pipeline_test, docker_test ]
|
||||
# Publish semver tags as releases.
|
||||
tags: [ 'v*.*.*' ]
|
||||
|
||||
|
||||
2
.github/workflows/package-linux.yml
vendored
2
.github/workflows/package-linux.yml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- name: Install python dependencies
|
||||
run: |
|
||||
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 -r requirements.txt
|
||||
- name: build binary
|
||||
|
||||
152
Dockerfile
152
Dockerfile
@@ -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
|
||||
FROM build-${TARGETARCH}${TARGETVARIANT}
|
||||
FROM ghcr.io/ciromattia/kcc:docker-base-20230514
|
||||
LABEL com.kcc.name="Kindle Comic Converter"
|
||||
LABEL com.kcc.author="Ciro Mattia Gonano, Paweł Jastrzębski and Darodi"
|
||||
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.title="Kindle Comic Converter"
|
||||
|
||||
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
WORKDIR /app
|
||||
COPY . /opt/kcc
|
||||
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 && \
|
||||
cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
||||
RUN cat /opt/kcc/kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/'//g" > /IMAGE_VERSION
|
||||
|
||||
ENTRYPOINT ["/opt/kcc/kcc-c2e.py"]
|
||||
CMD ["-h"]
|
||||
162
Dockerfile-base
Normal file
162
Dockerfile-base
Normal 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
124
README.md
@@ -136,89 +136,81 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
|
||||
### Standalone `kcc-c2e.py` usage:
|
||||
|
||||
```
|
||||
Usage: kcc-c2e [options] comic_file|comic_folder
|
||||
usage: kcc-c2e [options] [input]
|
||||
|
||||
Options:
|
||||
MAIN:
|
||||
-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]
|
||||
MANDATORY:
|
||||
input Full path to comic folder or file(s) to be processed.
|
||||
|
||||
OUTPUT SETTINGS:
|
||||
-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]
|
||||
MAIN:
|
||||
-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
|
||||
--ts TARGETSIZE, --targetsize TARGETSIZE
|
||||
the maximal size of output file in MB. [Default=100MB for webtoon and 400MB for others]
|
||||
|
||||
PROCESSING:
|
||||
-n, --noprocessing Do not modify image and ignore any profil or
|
||||
processing option
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
-r SPLITTER, --splitter=SPLITTER
|
||||
Double page parsing mode. 0: Split 1: Rotate 2: Both
|
||||
[Default=0]
|
||||
-g GAMMA, --gamma=GAMMA
|
||||
Apply gamma correction to linearize the image
|
||||
[Default=Auto]
|
||||
-c CROPPING, --cropping=CROPPING
|
||||
Set cropping mode. 0: Disabled 1: Margins 2: Margins +
|
||||
page numbers [Default=2]
|
||||
--cp=CROPPINGP, --croppingpower=CROPPINGP
|
||||
PROCESSING:
|
||||
-n, --noprocessing Do not modify image and ignore any profil or processing option
|
||||
-u, --upscale Resize images smaller than device's resolution
|
||||
-s, --stretch Stretch images to device's resolution
|
||||
-r SPLITTER, --splitter SPLITTER
|
||||
Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]
|
||||
-g GAMMA, --gamma GAMMA
|
||||
Apply gamma correction to linearize the image [Default=Auto]
|
||||
-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]
|
||||
--cm=CROPPINGM, --croppingminimum=CROPPINGM
|
||||
--cm CROPPINGM, --croppingminimum CROPPINGM
|
||||
Set cropping minimum area ratio [Default=0.0]
|
||||
--blackborders Disable autodetection and force black borders
|
||||
--whiteborders Disable autodetection and force white borders
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--mozjpeg Create JPEG files using mozJpeg
|
||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||
-d, --delete Delete source file(s) or a directory. It's not
|
||||
recoverable.
|
||||
--blackborders Disable autodetection and force black borders
|
||||
--whiteborders Disable autodetection and force white borders
|
||||
--forcecolor Don't convert images to grayscale
|
||||
--forcepng Create PNG files instead JPEG
|
||||
--mozjpeg Create JPEG files using mozJpeg
|
||||
--maximizestrips Turn 1x4 strips to 2x2 strips
|
||||
-d, --delete Delete source file(s) or a directory. It's not recoverable.
|
||||
|
||||
CUSTOM PROFILE:
|
||||
--customwidth=CUSTOMWIDTH
|
||||
OUTPUT SETTINGS:
|
||||
-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
|
||||
--customheight=CUSTOMHEIGHT
|
||||
--customheight CUSTOMHEIGHT
|
||||
Replace screen height provided by device profile
|
||||
|
||||
OTHER:
|
||||
-h, --help Show this help message and exit
|
||||
OTHER:
|
||||
-h, --help Show this help message and exit
|
||||
|
||||
```
|
||||
|
||||
### Standalone `kcc-c2p.py` usage:
|
||||
|
||||
```
|
||||
Usage: kcc-c2p [options] comic_folder
|
||||
usage: kcc-c2p [options] [input]
|
||||
|
||||
Options:
|
||||
MANDATORY:
|
||||
-y HEIGHT, --height=HEIGHT
|
||||
MANDATORY:
|
||||
input Full path to comic folder(s) to be processed. Separate multiple inputs with spaces.
|
||||
|
||||
MAIN:
|
||||
-y HEIGHT, --height HEIGHT
|
||||
Height of the target device screen
|
||||
-i, --in-place Overwrite source directory
|
||||
-m, --merge Combine every directory into a single image before
|
||||
splitting
|
||||
-i, --in-place Overwrite source directory
|
||||
-m, --merge Combine every directory into a single image before splitting
|
||||
|
||||
OTHER:
|
||||
-d, --debug Create debug file for every split image
|
||||
-h, --help Show this help message and exit
|
||||
OTHER:
|
||||
-d, --debug Create debug file for every split image
|
||||
-h, --help Show this help message and exit
|
||||
```
|
||||
|
||||
## CREDITS
|
||||
|
||||
15
environment.yml
Normal file
15
environment.yml
Normal 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
|
||||
@@ -19,8 +19,9 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
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)
|
||||
|
||||
from multiprocessing import freeze_support, set_start_method
|
||||
|
||||
@@ -19,8 +19,9 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
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)
|
||||
|
||||
from multiprocessing import freeze_support, set_start_method
|
||||
|
||||
5
kcc.py
5
kcc.py
@@ -19,8 +19,9 @@
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
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)
|
||||
|
||||
# OS specific workarounds
|
||||
|
||||
@@ -16,11 +16,12 @@
|
||||
# OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
# PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import unquote
|
||||
from urllib.request import urlretrieve
|
||||
from urllib.request import urlretrieve, urlopen
|
||||
from time import sleep
|
||||
from shutil import move, rmtree
|
||||
from subprocess import STDOUT, PIPE
|
||||
@@ -142,64 +143,27 @@ class VersionThread(QtCore.QThread):
|
||||
self.wait()
|
||||
|
||||
def run(self):
|
||||
# TODO adapt with github releases
|
||||
pass
|
||||
try:
|
||||
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:
|
||||
# XML = parse(urlopen(Request('https://kcc.iosphe.re/Version/',
|
||||
# headers={'User-Agent': 'KindleComicConverter/' + __version__})))
|
||||
# except Exception:
|
||||
# return
|
||||
# latestVersion = XML.childNodes[0].getElementsByTagName('LatestVersion')[0].childNodes[0].toxml()
|
||||
# if ("beta" not in __version__ and StrictVersion(latestVersion) > StrictVersion(__version__)) \
|
||||
# or ("beta" in __version__
|
||||
# and StrictVersion(latestVersion) >= StrictVersion(re.sub(r'-beta.*', '', __version__))):
|
||||
# if sys.platform.startswith('win'):
|
||||
# self.newVersion = latestVersion
|
||||
# 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)
|
||||
html_url = json_parser["html_url"]
|
||||
latest_version = json_parser["tag_name"]
|
||||
latest_version = re.sub(r'^v', "", latest_version)
|
||||
|
||||
if ("b" not in __version__ and StrictVersion(latest_version) > StrictVersion(__version__)) \
|
||||
or ("b" in __version__
|
||||
and StrictVersion(latest_version) >= StrictVersion(re.sub(r'b.*', '', __version__))):
|
||||
MW.addMessage.emit('<a href="' + html_url + '"><b>The new version is available!</b></a>', 'warning',
|
||||
False)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
def setAnswer(self, 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):
|
||||
def __init__(self):
|
||||
@@ -255,7 +219,7 @@ class WorkerThread(QtCore.QThread):
|
||||
MW.modeConvert.emit(0)
|
||||
|
||||
parser = comic2ebook.makeParser()
|
||||
options, _ = parser.parse_args()
|
||||
options = parser.parse_args()
|
||||
argv = ''
|
||||
currentJobs = []
|
||||
|
||||
@@ -506,7 +470,8 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
fnames = QtWidgets.QFileDialog.getOpenFileNames(MW, 'Select file', self.lastPath,
|
||||
'Comic (*.cbz *.cbr *.cb7 *.zip *.rar *.7z *.pdf);;All (*.*)')
|
||||
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]:
|
||||
if fname != '':
|
||||
if sys.platform.startswith('win'):
|
||||
@@ -617,7 +582,7 @@ class KCCGUI(KCC_ui.Ui_mainWindow):
|
||||
|
||||
def togglecroppingBox(self, value):
|
||||
if value:
|
||||
GUI.croppingWidget.setVisible(True)
|
||||
GUI.croppingWidget.setVisible(True)
|
||||
else:
|
||||
GUI.croppingWidget.setVisible(False)
|
||||
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)
|
||||
elif kind == 'question':
|
||||
GUI.versionCheck.setAnswer(QtWidgets.QMessageBox.question(MW, 'KCC - Question', message,
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No))
|
||||
QtWidgets.QMessageBox.Yes,
|
||||
QtWidgets.QMessageBox.No))
|
||||
|
||||
def updateProgressbar(self, command):
|
||||
if command == 'tick':
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from time import strftime, gmtime
|
||||
from copy import copy
|
||||
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 tempfile import mkdtemp, gettempdir, TemporaryFile
|
||||
from shutil import move, copytree, rmtree, copyfile
|
||||
from optparse import OptionParser, OptionGroup
|
||||
from multiprocessing import Pool
|
||||
from uuid import uuid4
|
||||
from slugify import slugify as slugifyExt
|
||||
from slugify import slugify as slugify_ext
|
||||
from PIL import Image
|
||||
from subprocess import STDOUT, PIPE
|
||||
from psutil import Popen, virtual_memory, disk_usage
|
||||
@@ -54,23 +54,23 @@ from . import __version__
|
||||
def main(argv=None):
|
||||
global options
|
||||
parser = makeParser()
|
||||
optionstemplate, args = parser.parse_args(argv)
|
||||
if len(args) == 0:
|
||||
args = parser.parse_args(argv)
|
||||
options = copy(args)
|
||||
if not argv or options.input == []:
|
||||
parser.print_help()
|
||||
return 0
|
||||
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:
|
||||
sources = set(args)
|
||||
sources = set(options.input)
|
||||
if len(sources) == 0:
|
||||
print('No matching files found.')
|
||||
return 1
|
||||
for source in sources:
|
||||
source = source.rstrip('\\').rstrip('/')
|
||||
options = copy(optionstemplate)
|
||||
options = copy(args)
|
||||
options = checkOptions(options)
|
||||
if len(sources) > 1:
|
||||
print('Working on ' + source + '...')
|
||||
print('Working on ' + source + '...')
|
||||
makeBook(source)
|
||||
return 0
|
||||
|
||||
@@ -546,7 +546,7 @@ def imgDirectoryProcessing(path):
|
||||
GUI.progressBarTick.emit(str(pagenumber))
|
||||
if len(work) > 0:
|
||||
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.join()
|
||||
if GUI and not GUI.conversionAlive:
|
||||
@@ -910,9 +910,9 @@ def createNewTome():
|
||||
|
||||
def slugify(value, 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:
|
||||
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))
|
||||
return value
|
||||
|
||||
@@ -933,85 +933,84 @@ def makeZIP(zipfilename, basedir, isepub=False):
|
||||
|
||||
|
||||
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")
|
||||
processingOptions = OptionGroup(psr, "PROCESSING")
|
||||
outputOptions = OptionGroup(psr, "OUTPUT SETTINGS")
|
||||
customProfileOptions = OptionGroup(psr, "CUSTOM PROFILE")
|
||||
otherOptions = OptionGroup(psr, "OTHER")
|
||||
mandatory_options = psr.add_argument_group("MANDATORY")
|
||||
main_options = psr.add_argument_group("MAIN")
|
||||
processing_options = psr.add_argument_group("PROCESSING")
|
||||
output_options = psr.add_argument_group("OUTPUT SETTINGS")
|
||||
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",
|
||||
help="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]")
|
||||
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]")
|
||||
mandatory_options.add_argument("input", action="extend", nargs="*", default=None,
|
||||
help="Full path to comic folder or file(s) to be processed.")
|
||||
|
||||
outputOptions.add_option("-o", "--output", action="store", dest="output", default=None,
|
||||
help="Output generated file to specified directory or file")
|
||||
outputOptions.add_option("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||
help="Comic title [Default=filename or directory name]")
|
||||
outputOptions.add_option("-f", "--format", action="store", dest="format", default="Auto",
|
||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
|
||||
"[Default=Auto]")
|
||||
outputOptions.add_option("-b", "--batchsplit", type="int", dest="batchsplit", default="0",
|
||||
help="Split output into multiple files. 0: Don't split 1: Automatic mode "
|
||||
"2: Consider every subdirectory as separate volume [Default=0]")
|
||||
main_options.add_argument("-p", "--profile", action="store", dest="profile", default="KV",
|
||||
help="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]")
|
||||
main_options.add_argument("-m", "--manga-style", action="store_true", dest="righttoleft", default=False,
|
||||
help="Manga style (right-to-left reading and splitting)")
|
||||
main_options.add_argument("-q", "--hq", action="store_true", dest="hq", default=False,
|
||||
help="Try to increase the quality of magnification")
|
||||
main_options.add_argument("-2", "--two-panel", action="store_true", dest="autoscale", default=False,
|
||||
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,
|
||||
help="Do not modify image and ignore any profil or processing option")
|
||||
processingOptions.add_option("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||
help="Resize images smaller than device's resolution")
|
||||
processingOptions.add_option("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
||||
help="Stretch images to device's resolution")
|
||||
processingOptions.add_option("-r", "--splitter", type="int", dest="splitter", default="0",
|
||||
help="Double page parsing mode. 0: Split 1: Rotate 2: Both [Default=0]")
|
||||
processingOptions.add_option("-g", "--gamma", type="float", dest="gamma", default="0.0",
|
||||
help="Apply gamma correction to linearize the image [Default=Auto]")
|
||||
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.")
|
||||
output_options.add_argument("-o", "--output", action="store", dest="output", default=None,
|
||||
help="Output generated file to specified directory or file")
|
||||
output_options.add_argument("-t", "--title", action="store", dest="title", default="defaulttitle",
|
||||
help="Comic title [Default=filename or directory name]")
|
||||
output_options.add_argument("-f", "--format", action="store", dest="format", default="Auto",
|
||||
help="Output format (Available options: Auto, MOBI, EPUB, CBZ, KFX, MOBI+EPUB) "
|
||||
"[Default=Auto]")
|
||||
output_options.add_argument("-b", "--batchsplit", type=int, dest="batchsplit", default="0",
|
||||
help="Split output into multiple files. 0: Don't split 1: Automatic mode "
|
||||
"2: Consider every subdirectory as separate volume [Default=0]")
|
||||
|
||||
customProfileOptions.add_option("--customwidth", type="int", dest="customwidth", default=0,
|
||||
help="Replace screen width provided by device profile")
|
||||
customProfileOptions.add_option("--customheight", type="int", dest="customheight", default=0,
|
||||
help="Replace screen height provided by device profile")
|
||||
processing_options.add_argument("-n", "--noprocessing", action="store_true", dest="noprocessing", default=False,
|
||||
help="Do not modify image and ignore any profil or processing option")
|
||||
processing_options.add_argument("-u", "--upscale", action="store_true", dest="upscale", default=False,
|
||||
help="Resize images smaller than device's resolution")
|
||||
processing_options.add_argument("-s", "--stretch", action="store_true", dest="stretch", default=False,
|
||||
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",
|
||||
help="Show this help message and exit")
|
||||
custom_profile_options.add_argument("--customwidth", type=int, dest="customwidth", default=0,
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from shutil import rmtree, copytree, move
|
||||
from optparse import OptionParser, OptionGroup
|
||||
from multiprocessing import Pool
|
||||
from PIL import Image, ImageChops, ImageOps, ImageDraw
|
||||
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
|
||||
@@ -102,7 +102,7 @@ def splitImage(work):
|
||||
opt = work[2]
|
||||
filePath = os.path.join(path, name)
|
||||
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
|
||||
Image.MAX_IMAGE_PIXELS = 1000000000
|
||||
Image.MAX_IMAGE_PIXELS = 1000000000
|
||||
imgOrg = Image.open(filePath).convert('RGB')
|
||||
imgProcess = Image.open(filePath).convert('1')
|
||||
widthImg, heightImg = imgOrg.size
|
||||
@@ -116,7 +116,7 @@ def splitImage(work):
|
||||
panelDetected = False
|
||||
panels = []
|
||||
while yWork < heightImg:
|
||||
tmpImg = imgProcess.crop([4, yWork, widthImg-4, yWork + 4])
|
||||
tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4))
|
||||
solid = detectSolid(tmpImg)
|
||||
if not solid and not panelDetected:
|
||||
panelDetected = True
|
||||
@@ -149,7 +149,7 @@ def splitImage(work):
|
||||
|
||||
if opt.debug:
|
||||
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.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
|
||||
|
||||
@@ -182,7 +182,7 @@ def splitImage(work):
|
||||
if pageHeight > 15:
|
||||
newPage = Image.new('RGB', (widthImg, pageHeight))
|
||||
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))
|
||||
targetHeight += panelsProcessed[panel][2]
|
||||
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):
|
||||
global options, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
||||
parser = OptionParser(usage="Usage: kcc-c2p [options] comic_folder", add_help_option=False)
|
||||
mainOptions = OptionGroup(parser, "MANDATORY")
|
||||
otherOptions = OptionGroup(parser, "OTHER")
|
||||
mainOptions.add_option("-y", "--height", type="int", dest="height", default=0,
|
||||
help="Height of the target device screen")
|
||||
mainOptions.add_option("-i", "--in-place", action="store_true", dest="inPlace", default=False,
|
||||
help="Overwrite source directory")
|
||||
mainOptions.add_option("-m", "--merge", action="store_true", dest="merge", default=False,
|
||||
help="Combine every directory into a single image before splitting")
|
||||
otherOptions.add_option("-d", "--debug", action="store_true", dest="debug", default=False,
|
||||
help="Create debug file for every split image")
|
||||
otherOptions.add_option("-h", "--help", action="help",
|
||||
help="Show this help message and exit")
|
||||
parser.add_option_group(mainOptions)
|
||||
parser.add_option_group(otherOptions)
|
||||
options, args = parser.parse_args(argv)
|
||||
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
|
||||
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
|
||||
|
||||
mandatory_options = parser.add_argument_group("MANDATORY")
|
||||
main_options = parser.add_argument_group("MAIN")
|
||||
other_options = parser.add_argument_group("OTHER")
|
||||
mandatory_options.add_argument("input", action="extend", nargs="*", default=None,
|
||||
help="Full path to comic folder(s) to be processed. Separate multiple inputs"
|
||||
" with spaces.")
|
||||
main_options.add_argument("-y", "--height", type=int, dest="height", default=0,
|
||||
help="Height of the target device screen")
|
||||
main_options.add_argument("-i", "--in-place", action="store_true", dest="inPlace", default=False,
|
||||
help="Overwrite source directory")
|
||||
main_options.add_argument("-m", "--merge", action="store_true", dest="merge", default=False,
|
||||
help="Combine every directory into a single image before splitting")
|
||||
other_options.add_argument("-d", "--debug", action="store_true", dest="debug", default=False,
|
||||
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:
|
||||
GUI = qtgui
|
||||
else:
|
||||
GUI = None
|
||||
if len(args) != 1:
|
||||
if not argv or args.input == []:
|
||||
parser.print_help()
|
||||
return 1
|
||||
if options.height > 0:
|
||||
options.sourceDir = args[0]
|
||||
options.targetDir = args[0] + "-Splitted"
|
||||
if os.path.isdir(options.sourceDir):
|
||||
rmtree(options.targetDir, True)
|
||||
copytree(options.sourceDir, options.targetDir)
|
||||
work = []
|
||||
pagenumber = 1
|
||||
splitWorkerOutput = []
|
||||
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||
if options.merge:
|
||||
print("Merging images...")
|
||||
directoryNumer = 1
|
||||
mergeWork = []
|
||||
mergeWorkerOutput = []
|
||||
mergeWorkerPool = Pool(maxtasksperchild=10)
|
||||
mergeWork.append([options.targetDir])
|
||||
for root, dirs, files in os.walk(options.targetDir, False):
|
||||
dirs, files = walkSort(dirs, files)
|
||||
for directory in dirs:
|
||||
directoryNumer += 1
|
||||
mergeWork.append([os.path.join(root, directory)])
|
||||
if args.height > 0:
|
||||
for sourceDir in args.input:
|
||||
targetDir = sourceDir + "-Splitted"
|
||||
if os.path.isdir(sourceDir):
|
||||
rmtree(targetDir, True)
|
||||
copytree(sourceDir, targetDir)
|
||||
work = []
|
||||
pagenumber = 1
|
||||
splitWorkerOutput = []
|
||||
splitWorkerPool = Pool(maxtasksperchild=10)
|
||||
if args.merge:
|
||||
print("Merging images...")
|
||||
directoryNumer = 1
|
||||
mergeWork = []
|
||||
mergeWorkerOutput = []
|
||||
mergeWorkerPool = Pool(maxtasksperchild=10)
|
||||
mergeWork.append([targetDir])
|
||||
for root, dirs, files in os.walk(targetDir, False):
|
||||
dirs, files = walkSort(dirs, files)
|
||||
for directory in dirs:
|
||||
directoryNumer += 1
|
||||
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:
|
||||
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(options.targetDir, True)
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(mergeWorkerOutput) > 0:
|
||||
rmtree(options.targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
|
||||
mergeWorkerOutput[0][1])
|
||||
print("Splitting images...")
|
||||
for root, _, files in os.walk(options.targetDir, False):
|
||||
for name in files:
|
||||
if getImageFileName(name) is not None:
|
||||
pagenumber += 1
|
||||
work.append([root, name, options])
|
||||
else:
|
||||
os.remove(os.path.join(root, name))
|
||||
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)
|
||||
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(targetDir, True)
|
||||
raise UserWarning("Conversion interrupted.")
|
||||
if len(splitWorkerOutput) > 0:
|
||||
rmtree(targetDir, True)
|
||||
raise RuntimeError("One of workers crashed. Cause: " + splitWorkerOutput[0][0],
|
||||
splitWorkerOutput[0][1])
|
||||
if args.inPlace:
|
||||
rmtree(sourceDir)
|
||||
move(targetDir, sourceDir)
|
||||
else:
|
||||
rmtree(targetDir, True)
|
||||
raise UserWarning("Source directory is empty.")
|
||||
else:
|
||||
rmtree(options.targetDir, True)
|
||||
raise UserWarning("Source directory is empty.")
|
||||
else:
|
||||
raise UserWarning("Provided path is not a directory.")
|
||||
raise UserWarning("Provided input is not a directory.")
|
||||
else:
|
||||
raise UserWarning("Target height is not set.")
|
||||
|
||||
@@ -55,7 +55,7 @@ class ComicArchive:
|
||||
|
||||
def extract(self, 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 + '" "' +
|
||||
self.filepath + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
|
||||
process.communicate()
|
||||
|
||||
@@ -24,6 +24,13 @@ import mozjpeg_lossless_optimization
|
||||
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
|
||||
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:
|
||||
def __init__(self):
|
||||
@@ -306,36 +313,32 @@ class ComicPage:
|
||||
self.image = self.image.quantize(palette=palImg)
|
||||
|
||||
def resizeImage(self):
|
||||
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||
method = Image.Resampling.BICUBIC
|
||||
else:
|
||||
method = Image.Resampling.LANCZOS
|
||||
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()
|
||||
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)
|
||||
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:
|
||||
borderw = int((self.size[0] - self.image.size[0]) / 2)
|
||||
borderh = int((self.size[1] - self.image.size[1]) / 2)
|
||||
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]:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=Image.Resampling.BICUBIC, centering=(0.5, 0.5))
|
||||
else:
|
||||
if self.opt.format == 'CBZ' or self.opt.kfx:
|
||||
ratioDev = float(self.size[0]) / float(self.size[1])
|
||||
if (float(self.image.size[0]) / float(self.image.size[1])) < ratioDev:
|
||||
diff = int(self.image.size[1] * ratioDev) - self.image.size[0]
|
||||
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))
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
else: # if image bigger than device resolution or smaller with upscaling
|
||||
if abs(ratio_image - ratio_device) < AUTO_CROP_THRESHOLD:
|
||||
self.image = ImageOps.fit(self.image, self.size, method=method)
|
||||
elif self.opt.format == 'CBZ' or self.opt.kfx:
|
||||
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
|
||||
else:
|
||||
hpercent = self.size[1] / float(self.image.size[1])
|
||||
wsize = int((float(self.image.size[0]) * float(hpercent)))
|
||||
self.image = self.image.resize((wsize, self.size[1]), method)
|
||||
if self.image.size[0] > self.size[0] or self.image.size[1] > self.size[1]:
|
||||
self.image.thumbnail(self.size, Image.Resampling.LANCZOS)
|
||||
self.image = ImageOps.contain(self.image, self.size, method=method)
|
||||
|
||||
def resize_method(self):
|
||||
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
|
||||
method = Image.Resampling.BICUBIC
|
||||
else:
|
||||
method = Image.Resampling.LANCZOS
|
||||
return method
|
||||
|
||||
def getBoundingBox(self, tmptmg):
|
||||
min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size]
|
||||
|
||||
Reference in New Issue
Block a user