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

Compare commits

..

32 Commits

Author SHA1 Message Date
darodi
b8a5582431 update Dockerfile-base file version 2023-08-09 18:40:32 +02:00
Alex Xu
9e73f065ae Merge branch 'ciromattia:master' into pyqt6 2023-08-08 14:38:44 -07:00
Alex Xu
b40cfcd9a6 Merge branch 'ciromattia:master' into pyqt6 2023-08-07 20:55:52 -07:00
Alex Xu
42c79b326f remove mozJpeg 2023-07-31 08:25:31 -07:00
Alex Xu
4cb986c302 remove space 2023-07-25 14:10:09 -07:00
Alex Xu
69856c7f48 use same settings save location as qt5 2023-07-25 14:02:02 -07:00
Alex Xu
a3dd23ed40 Merge branch 'master' into pyqt6 2023-07-25 13:28:23 -07:00
Alex Xu
5a21ff0bcf fix state issue 2023-07-23 15:25:35 -07:00
Alex Xu
17e8ffb7c0 add warning text 2023-07-12 12:45:17 -07:00
Alex Xu
4499e8faa0 add mozjpeg to gitignore 2023-07-05 07:06:06 -07:00
Alex Xu
6ee685e64c Update README.md 2023-07-04 08:47:16 -07:00
Alex Xu
9f34df1414 Update README.md 2023-07-04 08:45:20 -07:00
Alex Xu
6a6a363c47 Update package-linux.yml 2023-07-01 11:25:28 -07:00
Alex Xu
6ba2ef66ca Update README.md 2023-07-01 11:24:43 -07:00
Alex Xu
264184c7e0 Update Dockerfile-base 2023-07-01 11:24:13 -07:00
Alex Xu
7e62329a51 Update package-linux.yml 2023-07-01 11:22:18 -07:00
Alex Xu
7e82f492c6 Update package-linux.yml 2023-07-01 11:21:56 -07:00
Alex Xu
829334556f add mozJpeg warning 2023-07-01 08:31:56 -07:00
Alex Xu
5c1408e7b7 remove references to qt5 2023-06-30 10:57:27 -07:00
Alexander Xu
b51c87e3bc import CheckedState 2023-06-30 10:39:19 -07:00
Alexander Xu
ada3232a0a fix batch 2023-06-30 10:31:43 -07:00
Alexander Xu
44682156c9 add mozJpeg 2023-06-30 10:29:55 -07:00
Alexander Xu
242fd70b54 Add CheckState enums 2023-06-30 10:19:39 -07:00
Alexander Xu
3f2365c677 edit shared 2023-06-30 09:37:03 -07:00
Alexander Xu
f4e45e6052 change exec 2023-06-30 09:35:02 -07:00
Alexander Xu
fe81af831e add comment back 2023-06-30 09:30:50 -07:00
Alexander Xu
eeec0501f5 add spaces 2023-06-30 09:29:34 -07:00
Alexander Xu
3d0d615879 fix tray icon 2023-06-30 09:28:03 -07:00
Alexander Xu
82bc405f6f pyside6 2023-06-30 09:10:55 -07:00
Alexander Xu
b2a079c958 fix epub icon 2023-06-28 14:54:14 -07:00
Alex Xu
b37ea52c7d Merge branch 'master' into pyqt6 2023-06-28 14:52:20 -07:00
Alex Xu
d668883b6f initial upgrade 2023-05-21 17:13:39 -07:00
65 changed files with 2649 additions and 7341 deletions

View File

@@ -1,40 +1,14 @@
.git
.github
build
dist
KindleComicConverter.egg-info
.dockerignore
.gitignore
.travis.yml
Dockerfile
other
venv
.venv
__pycache__/
*/__pycache__/
*.pyc
*.md
*.txt
!requirements-docker.txt
MANIFEST.in
*.yml
*.spec
*.svg
*.jpg
*.json
gen_ui_files.bat
gen_ui_files.sh
gui/
icons/
kindlecomicconverter/KCC_gui.py
kindlecomicconverter/KCC_rc.py
kindlecomicconverter/KCC_ui_editor.py
kindlecomicconverter/KCC_ui.py
LICENSE.txt
MANIFEST.in

15
.github/FUNDING.yml vendored
View File

@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: eink_dude
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
Add a screenshot of your KCC settings.
**Desktop (please complete the following information):**
- OS: [e.g. macOS, Linux, Windows 11]
- Device [e.g. Kindle Paperwhite 3rd gen, Kobo Libra 2]
**Additional context**
Add any other context about the problem here.

View File

@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -69,6 +69,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

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

@@ -1,10 +1,10 @@
name: Build and Publish Docker Image
name: Docker
on:
workflow_dispatch:
push:
tags:
- 'v*.*.*'
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
# Don't trigger if it's just a documentation update
paths-ignore:
@@ -15,53 +15,19 @@ on:
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build_and_publish_base_image:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set Release Date
id: release_date
run: |
echo "release_date=$(date --rfc-3339=date)" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/kcc
# Always creates the "latest" tag
flavor: |
latest=true
tags: |
type=ref,event=tag
type=raw,value=${{ steps.release_date.outputs.release_date }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
context: .
push: true
tags: |
${{ steps.meta.outputs.tags }}
cache-from: |
type=registry,ref=ghcr.io/ciromattia/kcc:cache
type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache
cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/kcc:cache,mode=max
build_and_push:
uses: sdr-enthusiasts/common-github-workflows/.github/workflows/build_and_push_image.yml@main
with:
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 }}
secrets:
ghcr_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -23,18 +23,18 @@ on:
jobs:
build:
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: 'pip'
- name: Install python dependencies
run: |
sudo apt-get update
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2 libxcb-cursor0
sudo apt-get install -y libpng-dev libjpeg-dev p7zip-full p7zip-rar python3-pip squashfs-tools libfuse2
python -m pip install --upgrade pip setuptools wheel certifi pyinstaller --no-binary pyinstaller
python -m pip install -r requirements.txt
- name: build binary
@@ -59,16 +59,17 @@ jobs:
env:
UPDATE_INFO: gh-releases-zsync|ciromattia|kcc|latest|*x86_64.AppImage.zsync
- name: upload artifact
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v3
with:
name: AppImage
path: './*.AppImage*'
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: false
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
*.AppImage*

View File

@@ -23,16 +23,11 @@ on:
jobs:
build:
strategy:
matrix:
os: [ macos-15-intel, macos-14 ]
runs-on: ${{ matrix.os }}
env:
MACOSX_DEPLOYMENT_TARGET: '14.0'
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: 'pip'
@@ -71,7 +66,7 @@ jobs:
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- uses: actions/setup-node@v6
- uses: actions/setup-node@v3
with:
node-version: 16
- run: npm install -g appdmg
@@ -80,17 +75,19 @@ jobs:
run: |
python setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v3
with:
name: mac-os-build-${{ runner.arch }}
name: mac-os-build
path: dist/*.dmg
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: false
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/*.dmg
- name: Clean up keychain and provisioning profile
# TODO signing
@@ -98,4 +95,4 @@ jobs:
# if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
rm ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision

View File

@@ -1,66 +0,0 @@
name: build KCC for osx legacy
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- '**.sh'
- 'docs/**'
- 'Dockerfile'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build:
strategy:
matrix:
os: [ macos-15-intel ]
runs-on: ${{ matrix.os }}
env:
# We need the official Python, because the GA ones only support newer macOS versions
# The deployment target is picked up by the Python build tools automatically
PYTHON_VERSION: 3.11.9
MACOSX_DEPLOYMENT_TARGET: '10.14'
steps:
- uses: actions/checkout@v6
- name: Get Python
run: curl https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg -o "python.pkg"
- name: Install Python
run: |
sudo installer -pkg python.pkg -target /
- name: Install Python dependencies
run: |
python3 --version
pip3 install --upgrade pip setuptools wheel pyinstaller certifi
pip3 install --upgrade -r requirements-osx-legacy.txt
./gen_ui_files.sh
- uses: actions/setup-node@v6
with:
node-version: 16
- run: npm install -g appdmg
- name: build binary
run: |
python3 setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v6
with:
name: osx-build-${{ runner.arch }}
path: dist/*.dmg
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: false
files: |
LICENSE.txt
dist/*.dmg

View File

@@ -0,0 +1,63 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build KCC for windows with docker
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# - name: Set up Python
# uses: actions/setup-python@v4
# with:
# python-version: 3.11
# cache: 'pip'
# - name: Install python dependencies
# run: |
# python -m pip install --upgrade pip setuptools wheel pyinstaller
# pip install -r requirements.txt
# - name: build binary
# run: |
# pyi-makespec -F -i icons\\comic2ebook.ico -n KCC_test -w --noupx kcc.py
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc.spec
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2e.spec
- name: Package Application
uses: JackMcKew/pyinstaller-action-windows@main
with:
path: .
spec: ./kcc-c2p.spec
- name: rename binaries
run: |
version_built=$(cat kindlecomicconverter/__init__.py | grep version | awk '{print $3}' | sed "s/[^.0-9b]//g")
mv dist/windows/kcc.exe dist/windows/kcc_${version_built}.exe
mv dist/windows/kcc-c2e.exe dist/windows/kcc-c2e_${version_built}.exe
mv dist/windows/kcc-c2p.exe dist/windows/kcc-c2p_${version_built}.exe
- name: upload build
uses: actions/upload-artifact@v3
with:
name: windows-build
path: dist/windows/*.exe
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: true
files: |
CHANGELOG.md
LICENSE.txt
dist/windows/*.exe

View File

@@ -23,21 +23,11 @@ on:
jobs:
build:
strategy:
matrix:
entry: [ kcc, kcc-c2e, kcc-c2p ]
include:
- entry: kcc
command: build_binary
- entry: kcc-c2e
command: build_c2e
- entry: kcc-c2p
command: build_c2p
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@v4
with:
python-version: 3.11
cache: 'pip'
@@ -50,29 +40,19 @@ jobs:
pip install certifi pyinstaller --no-binary pyinstaller
- name: build binary
run: |
python setup.py ${{ matrix.command }}
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v6
python setup.py build_binary
- name: upload build
uses: actions/upload-artifact@v3
with:
name: windows-build-${{ matrix.entry }}
name: windows-build
path: dist/*.exe
- id: optional_step_id
uses: signpath/github-action-submit-signing-request@v2.0
if: ${{ github.repository == 'ciromattia/kcc' }}
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '1dc1bad6-4a8c-4f85-af30-5c5d3d392ea6'
project-slug: 'kcc'
signing-policy-slug: 'release-signing'
github-artifact-id: '${{ steps.upload-unsigned-artifact.outputs.artifact-id }}'
wait-for-completion: true
output-artifact-directory: 'dist/'
- name: Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: false
generate_release_notes: true
files: |
dist/*.exe
CHANGELOG.md
LICENSE.txt
dist/*.exe

View File

@@ -1,60 +0,0 @@
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: build KCC for windows 7
on:
workflow_dispatch:
push:
tags:
- "v*.*.*"
# Don't trigger if it's just a documentation update
paths-ignore:
- '**.md'
- '**.MD'
- '**.yml'
- '**.sh'
- 'docs/**'
- 'Dockerfile'
- 'LICENSE'
- '.gitattributes'
- '.gitignore'
- '.dockerignore'
jobs:
build:
runs-on: windows-2022
env:
WINDOWS_7: 1
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.8
cache: 'pip'
- name: Install dependencies
env:
PYINSTALLER_COMPILE_BOOTLOADER: 1
run: |
python -m pip install --upgrade pip setuptools wheel
pip install -r requirements-win7.txt
pip install certifi pyinstaller --no-binary pyinstaller
.\gen_ui_files.bat
- name: build binary
run: |
python setup.py build_binary
- name: upload-unsigned-artifact
id: upload-unsigned-artifact
uses: actions/upload-artifact@v6
with:
name: windows7-build
path: dist/*.exe
- name: Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
prerelease: true
generate_release_notes: false
files: |
dist/*.exe

3
.gitignore vendored
View File

@@ -8,9 +8,6 @@ dist/
build/
KindleComicConverter*.egg-info/
.idea/
.vscode/
win7
osx10.11
/venv/
/kindlegen*
/kcc.bat

30
.travis.yml Normal file
View File

@@ -0,0 +1,30 @@
matrix:
include:
- os: osx
language: generic
osx_image: xcode11.1
before_install:
- pip3 install --upgrade pip setuptools wheel
install:
- pip3 install -r requirements.txt
- pip3 install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip
- npm install -g appdmg
script: python3 setup.py build_binary
before_deploy:
- shopt -s extglob
- rm -r dist/!(*.deb|*.dmg)
deploy:
provider: gcs
access_key_id: GOOG1EC62457RKUYFR2TIZUWV4EFSV2EP5LVLPPFXUAKADWJFDYPFW63BQSLA
secret_access_key:
secure: sxYjeho7U3im0Ezf6cz6TjYDiLvf0kAM2ETQHYoFNbD1VVvhJJyymDCnPH80zpFKmhc1MWTB6ndwsrPfcyZDLR2meSdWGPjZfFPY3RcrfImndKi7ln+mYQDBQ7W1lGit4YcH3Ju7LHceaTbRA7fVTX8pWKOcbXL2oM+lQxTJHH32+crVma+ChhbjzTWsSLRoakt3Nhiveec5p/qSW7AFe4Zq+b3C85IgwjSJI/xVwzaWrs6p915h1zZi7KL7YCMIxfQFrvRPFR2KTbh/DoLCCrqfbD4qh0PVy1li51Ac3hd/u3foiNnTNchzgE3Nv/nbKmtFU6huuLNgzkQGuLA+yn7mKYzBwA3ZmFgoimdH9+yRCMkZ8B5VHpvfN1hgpJcyEl1T98Kv4cdtRYNB4w9iAMy1qSVxhjeI+2rjuWGoXro0lU6L4LIRCOruY3AuLCAKG8Qw5Ak9ksmIKBhZ9soxpoIwu/TYDUQkFj29IrUQucg9TEp7uAoxu8/7EHxB7hWnBRaBAAQbMuIRg7yysT3FT0Os6SB0t9+RBsVMSPuIti9JJZ2Lu0uRI1+Se+g7ItzYtJoPhBJAzAa+J9OONj0RNj2z8Vq2oIBhH4z6b6zTRMVroos3cdfYl5qIKs9SQ7rmeHoPRROcqpCznsUZ/ESa4f2MewFU/7AYcEnCesZV4xg=
bucket: kcc-deploy
local-dir: dist
skip_cleanup: true
on:
repo: AcidWeb/KCC

View File

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

160
Dockerfile-base Normal file
View File

@@ -0,0 +1,160 @@
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 && \
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+=(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+=(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-20230809 > /IMAGE_VERSION

View File

@@ -1,9 +1,8 @@
ISC LICENSE
Copyright (c) 2012-2025 Ciro Mattia Gonano <ciromattia@gmail.com>
Copyright (c) 2012-2014 Ciro Mattia Gonano <ciromattia@gmail.com>
Copyright (c) 2013-2019 Paweł Jastrzębski <pawelj@iosphe.re>
Copyright (c) 2021-2023 Darodi (https://github.com/darodi)
Copyright (c) 2023-2025 Alex Xu (https://github.com/axu2)
Copyright (c) 2021-2023 Darodi
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the

1
MANIFEST.in Normal file
View File

@@ -0,0 +1 @@
exclude kindlecomicconverter/sentry.py

352
README.md
View File

@@ -1,62 +1,15 @@
<img src="header.jpg" alt="Header Image" width="400">
# KCC
[![GitHub release](https://img.shields.io/github/release/ciromattia/kcc.svg)](https://github.com/ciromattia/kcc/releases)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ciromattia/kcc/docker-publish.yml?label=docker%20build)](https://github.com/ciromattia/kcc/pkgs/container/kcc)
[![Github All Releases](https://img.shields.io/github/downloads/ciromattia/kcc/total.svg)](https://github.com/ciromattia/kcc/releases)
**Kindle Comic Converter** optimizes black & white (or color) comics and manga for E-ink ereaders
like Kindle, Kobo, ReMarkable, and more.
Pages display in fullscreen without margins,
with proper fixed layout support.
Supported input formats include JPG/PNG image files in folders, archives, or PDFs.
Supported output formats include MOBI/AZW3, EPUB, KEPUB, CBZ, and PDF.
KCC runs on Windows, macOS, and Linux.
[![GitHub release](https://img.shields.io/github/release/ciromattia/kcc.svg)](https://github.com/ciromattia/kcc/releases)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/ciromattia/kcc/docker-publish.yml?label=docker%20build)](https://github.com/ciromattia/kcc/pkgs/container/kcc)
Just drop your input files into the KCC window, hit convert, and USB drop the output files onto your device's `documents` folder!
https://github.com/user-attachments/assets/da73d625-e082-482d-91a4-ae4765e96fd7
**WARNING**: Kindle Scribe 2025 support may not be possible. Does not work well currently.
**NEW**: PDF output is now supported for direct conversion to reMarkable devices!
When using a reMarkable profile (Rmk1, Rmk2, RmkPP), the format automatically defaults to PDF
for optimal compatibility with your device's native PDF reader.
The absolute highest quality source files are print quality DRM-free PDFs from Kodansha/[Humble Bundle](https://humblebundleinc.sjv.io/xL6Zv1)/Fanatical,
which can be directly converted by KCC.
Its main feature is various optional image processing steps to look good on eink screens,
which have different requirements than normal LCD screens.
Combining that with downscaling to your specific device's screen resolution
can result in filesize reductions of hundreds of MB per volume with no visible quality loss on eink.
This can also improve battery life, page turn speed, and general performance
on underpowered ereaders with small memory and storage capacities.
KCC avoids many common formatting issues (some of which occur [even on the Kindle Store](https://github.com/ciromattia/kcc/wiki/Kindle-Store-bad-formatting)), such as:
1) faded black levels causing unneccessarily low contrast, which is hard to see and can cause eyestrain.
2) unneccessary margins at the bottom of the screen
3) Not utilizing the full 1860x2480 resolution of the 10" Kindle Scribe
4) incorrect page turn direction for manga that's read right to left
5) unaligned two page spreads in landscape, where pages are shifted over by 1
6) Removing without blur the rainbow effect on color eink Kaleido 3 due to manga screentones
The GUI looks like this, built in Qt6, with my most commonly used settings:
![image](https://github.com/user-attachments/assets/36ad2131-6677-4559-bd6f-314a90c27218)
Simply drag and drop your files/folders into the KCC window,
adjust your settings (hover over each option to see details in a tooltip),
and hit convert to create ereader optimized files.
You can change the default output directory by holding `Shift` while clicking the convert button.
Then just drag and drop the generated output files onto your device's documents folder via USB.
If you are on macOS and use a 2022+ Kindle, you may need to use Amazon USB File Manager for Mac.
YouTube tutorial (please subscribe): https://www.youtube.com/watch?v=QQ6zJcMF2Iw
Installation tutorial: https://www.youtube.com/watch?v=IR2Fhcm9658
**Kindle Comic Converter** is a Python app to convert comic/manga files or folders to EPUB, Panel View MOBI or E-Ink optimized CBZ.
It was initially developed for Kindle but since version 4.6 it outputs valid EPUB 3.0 so _**despite its name, KCC is
actually a comic/manga to EPUB converter that every e-reader owner can happily use**_.
It can also optionally optimize images by applying a number of transformations.
### A word of warning
**KCC** _is not_ [Amazon's Kindle Comic Creator](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1001103761) nor is in any way endorsed by Amazon.
@@ -69,99 +22,54 @@ If you have some **technical** problems using KCC please [file an issue here](ht
If you can fix an open issue, fork & make a pull request.
If you find **KCC** valuable you can consider donating to the authors:
- Ciro Mattia Gonano (founder, active 2012-2014):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- Paweł Jastrzębski (active 2013-2019):
[![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
[![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
- Alex Xu (active 2023-Present)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Q5Q41BW8HS)
## Commissions
This section is subject to change:
Email (for commisions and inquiries): `kindle.comic.converter` gmail
- Ciro Mattia Gonano:
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=D8WNYNPBGDAS2)
- [![Donate Flattr](https://img.shields.io/badge/Donate-Flattr-green.svg)](http://flattr.com/thing/2260449/ciromattiakcc-on-GitHub)
- Paweł Jastrzębski:
- [![Donate PayPal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YTTJ4LK2JDHPS)
- [![Donate Bitcoin](https://img.shields.io/badge/Donate-Bitcoin-green.svg)](https://jastrzeb.ski/donate/)
## Sponsors
## INSTALLATION
- Free code signing on Windows provided by [SignPath.io](https://about.signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
## DOWNLOADS
### DOWNLOADS
You can find the latest binary at the following link:
- **https://github.com/ciromattia/kcc/releases**
- flatpak : https://flathub.org/apps/details/io.github.ciromattia.kcc
- Docker: https://github.com/ciromattia/kcc/pkgs/container/kcc
Click on **Assets** of the latest release.
more information on [installation](https://github.com/ciromattia/kcc/wiki/Installation)
You probably want either
- `KCC_*.*.*.exe` (Windows)
- `kcc_macos_arm_*.*.*.dmg` (recent Mac with Apple Silicon M1 chip or later)
- `kcc_macos_i386_*.*.*.dmg` (older Mac with Intel chip macOS 14+)
### DEPENDENCIES
Following software is required to run Linux version of **KCC** and/or bare sources:
- Python 3.3+
- [PySide6](https://pypi.org/project/PySide6/) 6.5.1+ (only needed for GUI)
- [Pillow](https://pypi.python.org/pypi/Pillow/) 4.0.0+ (5.2.0+ needed for WebP support)
- [psutil](https://pypi.python.org/pypi/psutil) 5.9.5+
- [python-slugify](https://pypi.python.org/pypi/python-slugify) 1.2.1+, <8.0.0
- [raven](https://pypi.python.org/pypi/raven) 6.0.0+ (only needed for GUI)
There are also legacy macOS 10.14+ and Windows 7 experimental versions available.
On Debian based distributions these two commands should install all needed dependencies:
The `c2e` and `c2p` versions are command line tools for power users.
On macOS, if you get a `can't be opened` error, follow: https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unknown-developer-mh40616/mac
For flatpak, Docker, and AppImage versions, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation
```bash
$ sudo apt-get install -y python3 python3-dev libpng-dev libjpeg-dev p7zip-full p7zip-rar unrar-free libgl1 && \
python -m pip install --upgrade pip && \
python -m pip install --upgrade -r requirements.txt
```
## FAQ
- Should I use Calibre?
- No. Calibre doesn't properly support fixed layout EPUB/MOBI, so modifying KCC output (even just metadata!) in Calibre can break the formatting.
Viewing KCC output in Calibre will also not work properly.
On 7th gen and later Kindles running firmware 5.15.1+, you can get cover thumbnails simply by USB dropping into documents folder.
On 6th gen and older, you can get cover thumbnails by keeping Kindle plugged in during conversion.
If you are careful to not modify the file however, you can still use Calibre, but direct USB dropping is reccomended.
- Blank pages?
- May happen when [using PNG with Kindle Scribe](https://github.com/ciromattia/kcc/issues/665) or [any format with a Kindle Colorsoft](https://github.com/ciromattia/kcc/issues/768). Solve by using JPG with Kindle Scribe or buying a Kobo Colour. Happens more often when turning pages really fast.
Going back a few pages and exiting and re-entering book should fix it temporarily.
- What output format should I use?
- MOBI for Kindles. CBZ for Kindle DX. CBZ for Koreader. KEPUB for Kobo. PDF for ReMarkable.
- All options have additional information in tooltips if you hover over the option.
- To get the converted book onto your Kindle/Kobo, just drag and drop the mobi/kepub into the documents folder on your Kindle/Kobo via USB
- Kindle panel view not working?
- Virtual panel view is enabled in Aa menu on your Kindle, not in KCC as of 7.4
- Right to left mode not working?
- RTL mode only affects splitting order for CBZ output. Your cbz reader itself sets the page turn direction.
- Colors inverted?
- Disable Kindle dark mode
- Cannot connect Kindle Scribe or 2024+ Kindle to macOS
- Use official MTP [Amazon USB File Transfer app](https://www.amazon.com/gp/help/customer/display.html/ref=hp_Connect_USB_MTP?nodeId=TCUBEdEkbIhK07ysFu)
(no login required). Works much better than previously recommended Android File Transfer. Cannot run simutaneously with other transfer apps.
- How to make AZW3 instead of MOBI?
- The `.mobi` file generated by KCC is a dual filetype, it's both MOBI and AZW3. The file extension is `.mobi` for compatibility reasons.
- [Windows 7 support](https://github.com/ciromattia/kcc/issues/678)
- Image too dark?
- The default gamma correction of 1.8 makes the image darker, and is useful for faded/gray artwork/text. Disable by setting gamma = 1.0
- Huge margins / slow page turns?
- You likely modified the file during transfer using a 3rd party app. Try simply dragging and dropping the final mobi/kepub file into the Kindle documents folder via USB.
## PREREQUISITES
#### Optional dependencies
- KindleGen ~~[(deprecated link)](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211)~~ v2.9+ (For MOBI generation)
- should be placed in a directory reachable by your _PATH_ or in _KCC_ directory
- `KindleGen` can be found in [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011)
- `KindleGen` can be also be found in [Kindle Comic Creator](https://www.amazon.com/b?node=23496309011)
- [7z](http://www.7-zip.org/download.html) *(For CBZ/ZIP, CBR/RAR, 7z/CB7 support)*
- Unrar (no rar in 7z on Fedora)
You'll need to install various tools to access important but optional features. Close and re-open KCC to get KCC to detect them.
### KindleGen
On Windows and macOS, install [Kindle Previewer](https://www.amazon.com/Kindle-Previewer/b?ie=UTF8&node=21381691011) and `kindlegen` will be autodetected from it.
If you have issues detecting it, get stuck on the MOBI conversion step, or use Linux AppImage or Flatpak, refer to the wiki: https://github.com/ciromattia/kcc/wiki/Installation#kindlegen
### 7-Zip
This is optional but will make conversions much faster.
This is required for certain files and advanced features.
KCC will ask you to install if needed.
Refer to the wiki to install: https://github.com/ciromattia/kcc/wiki/Installation#7-zip
## INPUT FORMATS
**KCC** can understand and convert, at the moment, the following input types:
@@ -187,46 +95,34 @@ sudo apt-get install python3 p7zip-full python3-pil python3-psutil python3-slugi
### Profiles:
```
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
'OTHER': ("Other", (0, 0), Palette16, 1.0),
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K578': ("Kindle", (600, 800), Palette16, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.8),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
'OTHER': ("Other", (0, 0), Palette16, 1.8),
```
### Standalone `kcc-c2e.py` usage:
```
@@ -237,8 +133,7 @@ MANDATORY:
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, KoCC, KoL, KoLC, KoF, KoS, KoE)
[Default=KV]
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
@@ -247,33 +142,24 @@ MAIN:
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 profile or processing option
--pdfextract Use legacy PDF image extraction method from KCC 8 and earlier.
-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]
--autolevel Set most common dark pixel value to be black point for leveling.
--noautocontrast Disable autocontrast
--colorautocontrast Force autocontrast for all pages. Skipped when near blacks and whites don't exist
-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]
--preservemargin After calculating crop, "back up" a specified percentage amount [Default=0]
--cm CROPPINGM, --croppingminimum CROPPINGM
Set cropping minimum area ratio [Default=0.0]
--ipc INTERPANELCROP, --interpanelcrop INTERPANELCROP
Crop empty sections. 0: Disabled 1: Horizontally 2: Both [Default=0]
--blackborders Disable autodetection and force black borders
--whiteborders Disable autodetection and force white borders
--coverfill Center-crop only the cover to fill target device screen
--forcecolor Don't convert images to grayscale
--forcepng Create PNG files instead JPEG
--mozjpeg Create JPEG files using mozJpeg
--jpeg-quality The JPEG quality, on a scale from 0 (worst) to 95 (best). Default 85 for most devices.
--maximizestrips Turn 1x4 strips to 2x2 strips
-d, --delete Delete source file(s) or a directory. It's not recoverable.
@@ -282,19 +168,10 @@ OUTPUT SETTINGS:
Output generated file to specified directory or file
-t TITLE, --title TITLE
Comic title [Default=filename or directory name]
--metadatatitle Write title using ComicInfo.xml or other embedded metadata. 0: Don't use Title from metadata 1: Combine Title with default schema 2: Use Title only [Default=0]
-a AUTHOR, --author AUTHOR
Author name [Default=KCC]
-f FORMAT, --format FORMAT
Output format (Available options: Auto, MOBI, EPUB, CBZ, PDF, KFX, MOBI+EPUB) [Default=Auto]
--nokepub If format is EPUB, output file with '.epub' extension rather than '.kepub.epub'
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]
--spreadshift Shift first page to opposite side in landscape for two page spread alignment
--norotate Do not rotate double page spreads in spread splitter option.
--rotatefirst Put rotated spread first in spread splitter option.
--filefusion Combines all input files into a single file.
--eraserainbow Erase rainbow effect on color eink screen by attenuating interfering frequencies
CUSTOM PROFILE:
--customwidth CUSTOMWIDTH
@@ -326,86 +203,8 @@ OTHER:
-h, --help Show this help message and exit
```
## INSTALL FROM SOURCE
This section is for developers who want to contribute to KCC or power users who want to run the latest code without waiting for an official release.
Easiest to use [GitHub Desktop](https://desktop.github.com) to clone your fork of the KCC repo. From GitHub Desktop, click on `Repository` in the toolbar, then `Command Prompt` (Windows)/`Terminal` (Mac) to open a window in the KCC repo.
Depending on your system [Python](https://www.python.org) may be called either `python` or `python3`. We use virtual environments (venv) to manage dependencies.
If you want to edit the code, a good code editor is [VS Code](https://code.visualstudio.com).
If you want to edit the `.ui` files, use `pyside6-designer` which is included in the `pip install pyside6`.
If new objects have been added, verify that correct tab order has been applied by using [Tab Order Editing Mode](https://doc.qt.io/qt-6/designer-tab-order.html).
Then use the `gen_ui_files` scripts to autogenerate the python UI.
An example PR adding a new checkbox is here: https://github.com/ciromattia/kcc/pull/785
video of adding a new checkbox: https://youtu.be/g3I8DU74C7g
Do not use `git merge` to merge master from upstream,
use the "Sync fork" button on your fork on GitHub in your branch
to avoid weird looking merges in pull requests.
When making changes, be aware of how your change might affect file splitting/chunking
or chapter alignment.
### Windows install from source
One time setup and running for the first time:
```
python -m venv venv
venv\Scripts\activate.bat
pip install -r requirements.txt
python kcc.py
```
Every time you close Command Prompt, you will need to re-activate the virtual environment and re-run:
```
venv\Scripts\activate.bat
python kcc.py
```
You can build a `.exe` of KCC like the downloads we offer with
```
python setup.py build_binary
```
### macOS install from source
If the system installed Python gives you issues, please install the latest Python from either brew or the official website.
One time setup and running for the first time:
```
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python kcc.py
```
Every time you close Terminal, you will need to reactivate the virtual environment and re-run:
```
source venv/bin/activate
python kcc.py
```
You can build a `.app` of KCC like the downloads we offer with
```
python setup.py build_binary
```
## CREDITS
**KCC** is made by
- [Ciro Mattia Gonano](http://github.com/ciromattia)
- [Paweł Jastrzębski](http://github.com/AcidWeb)
- [Darodi](http://github.com/darodi)
- [Alex Xu](http://github.com/axu2)
**KCC** is made by [Ciro Mattia Gonano](http://github.com/ciromattia), [Paweł Jastrzębski](http://github.com/AcidWeb) and [Darodi](http://github.com/darodi) .
This script born as a cross-platform alternative to `KindleComicParser` by **Dc5e** (published [here](http://www.mobileread.com/forums/showthread.php?t=192783)).
@@ -416,12 +215,6 @@ The app relies and includes the following scripts:
- Icon is by **Nikolay Verin** ([http://ncrow.deviantart.com/](http://ncrow.deviantart.com/)) and released under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/) License.
## SAMPLE FILES CREATED BY KCC
https://www.mediafire.com/folder/ixh40veo6hrc5/kcc_samples
Older links (dead):
* [Kindle Oasis 2 / 3](http://kcc.iosphe.re/Samples/Ubunchu!-KO.mobi)
* [Kindle Paperwhite 3 / 4 / Voyage / Oasis](http://kcc.iosphe.re/Samples/Ubunchu!-KV.mobi)
* [Kindle Paperwhite 1 / 2](http://kcc.iosphe.re/Samples/Ubunchu!-KPW.mobi)
@@ -434,15 +227,12 @@ Older links (dead):
## PRIVACY
**KCC** is initiating internet connections in two cases:
* During startup - Version check and announcement check.
* During startup - Version check.
* When error occurs - Automatic reporting on Windows and macOS.
## KNOWN ISSUES
Please check [wiki page](https://github.com/ciromattia/kcc/wiki/Known-issues).
## COPYRIGHT
Copyright (c) 2012-2025 Ciro Mattia Gonano, Paweł Jastrzębski, Darodi and Alex Xu.
Copyright (c) 2012-2023 Ciro Mattia Gonano, Paweł Jastrzębski and Darodi.
**KCC** is released under ISC LICENSE; see [LICENSE.txt](./LICENSE.txt) for further details.
## Verification
Impact-Site-Verification: ffe48fc7-4f0c-40fd-bd2e-59f4d7205180

14
appveyor.yml Normal file
View File

@@ -0,0 +1,14 @@
environment:
PYTHON: "C:\\Python37-x64"
install:
- set PATH="%PYTHON%\\Scripts";%PATH%
- "%PYTHON%\\python.exe -m pip install --upgrade pip setuptools wheel"
- "%PYTHON%\\python.exe -m pip install -r requirements.txt"
- "%PYTHON%\\python.exe -m pip install certifi https://github.com/pyinstaller/pyinstaller/archive/develop.zip"
build_script:
- "%PYTHON%\\python.exe setup.py build_binary"
artifacts:
- path: dist\KCC*

View File

@@ -1,22 +0,0 @@
#!/bin/sh
set -e
MODE=${KCC_MODE:-c2e}
case "$MODE" in
"c2e")
echo "Starting C2E..."
exec c2e "$@"
;;
"c2p")
echo "Starting C2P..."
exec c2p "$@"
;;
*)
echo "Error: Unknown mode '$MODE'" >&2
exit 1
;;
esac

15
environment.yml Normal file
View File

@@ -0,0 +1,15 @@
name: kcc
channels:
- conda-forge
- defaults
dependencies:
- python=3.11
- Pillow>=5.2.0
- psutil>=5.9.5
- python-slugify>=1.2.1
- raven>=6.0.0
- distro
- pip
- pip:
- mozjpeg-lossless-optimization>=1.1.2
- pyside6>=6.5.1

View File

@@ -1,3 +1,3 @@
pyside6-uic gui/KCC.ui --from-imports > kindlecomicconverter/KCC_ui.py
pyside6-uic gui/MetaEditor.ui --from-imports > kindlecomicconverter/KCC_ui_editor.py
pyside6-uic gui/KCC.ui > kindlecomicconverter/KCC_ui.py
pyside6-uic gui/MetaEditor.ui > kindlecomicconverter/KCC_ui_editor.py
pyside6-rcc gui/KCC.qrc > kindlecomicconverter/KCC_rc.py

View File

@@ -6,7 +6,6 @@
<file>../icons/Kobo.png</file>
<file>../icons/Other.png</file>
<file>../icons/Kindle.png</file>
<file>../icons/Rmk.png</file>
</qresource>
<qresource prefix="Formats">
<file>../icons/CBZ.png</file>
@@ -28,9 +27,4 @@
<file>../icons/document_new.png</file>
<file>../icons/folder_new.png</file>
</qresource>
<qresource prefix="Brand">
<file>../icons/kofi_symbol.png</file>
<file>../icons/Humble_H-Red.png</file>
<file>../icons/Bindle_Red.png</file>
</qresource>
</RCC>

1317
gui/KCC.ui

File diff suppressed because it is too large Load Diff

View File

@@ -62,66 +62,56 @@
<item row="1" column="1">
<widget class="QLineEdit" name="volumeLine"/>
</item>
<item row="3" column="0">
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Number:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="2" column="1">
<widget class="QLineEdit" name="numberLine"/>
</item>
<item row="4" column="0">
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Writer:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="3" column="1">
<widget class="QLineEdit" name="writerLine"/>
</item>
<item row="5" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Penciller:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="4" column="1">
<widget class="QLineEdit" name="pencillerLine"/>
</item>
<item row="6" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Inker:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<item row="5" column="1">
<widget class="QLineEdit" name="inkerLine"/>
</item>
<item row="7" column="0">
<item row="6" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Colorist:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="6" column="1">
<widget class="QLineEdit" name="coloristLine"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="titleLine"/>
</item>
</layout>
</widget>
</item>
@@ -192,18 +182,6 @@
</item>
</layout>
</widget>
<tabstops>
<tabstop>seriesLine</tabstop>
<tabstop>volumeLine</tabstop>
<tabstop>titleLine</tabstop>
<tabstop>numberLine</tabstop>
<tabstop>writerLine</tabstop>
<tabstop>pencillerLine</tabstop>
<tabstop>inkerLine</tabstop>
<tabstop>coloristLine</tabstop>
<tabstop>okButton</tabstop>
<tabstop>cancelButton</tabstop>
</tabstops>
<resources>
<include location="KCC.qrc"/>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

View File

@@ -20,17 +20,14 @@
import sys
from kcc import modify_path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
exit(1)
from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import startC2E
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
startC2E()

View File

@@ -8,10 +8,10 @@ a = Analysis(['kcc-c2e.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

View File

@@ -20,17 +20,14 @@
import sys
from kcc import modify_path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
exit(1)
from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import startC2P
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
startC2P()

View File

@@ -8,10 +8,10 @@ a = Analysis(['kcc-c2p.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

123
kcc.iss Normal file
View File

@@ -0,0 +1,123 @@
#define MyAppName "Kindle Comic Converter"
#define MyAppVersion "5.5.2"
#define MyAppPublisher "Ciro Mattia Gonano, Paweł Jastrzębski"
#define MyAppURL "http://kcc.iosphe.re/"
#define MyAppExeName "KCC.exe"
[Setup]
AppId={{7D279A59-C65E-4DA7-B165-56DD06596216}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
AppCopyright=Copyright (C) 2012-2019 Ciro Mattia Gonano and Paweł Jastrzębski
ArchitecturesAllowed=x64
DefaultDirName={pf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
LicenseFile=LICENSE.txt
OutputBaseFilename=KindleComicConverter_win_{#MyAppVersion}
SetupIconFile=icons\comic2ebook.ico
SolidCompression=yes
ShowLanguageDialog=no
LanguageDetectionMethod=none
WizardImageFile=icons\Wizard.bmp
WizardSmallImageFile=icons\Wizard-Small.bmp
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
ChangesAssociations=True
InfoAfterFile=other\windows\InstallWarning.rtf
SignTool=SignTool /d $q{#MyAppName}$q /du $q{#MyAppURL}$q $f
MinVersion=0,6.0
OutputDir=dist
ArchitecturesInstallIn64BitMode=x64
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "CBZassociation"; Description: "CBZ"; GroupDescription: "File associations:"
Name: "CBRassociation"; Description: "CBR"; GroupDescription: "File associations:"
Name: "CB7association"; Description: "CB7"; GroupDescription: "File associations:"
[Files]
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion solidbreak
Source: "other\windows\Additional-LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "other\windows\7z.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "other\windows\7z.dll"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\Readme"; Filename: "https://github.com/ciromattia/kcc#kcc"
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
[Messages]
WelcomeLabel1=Welcome to the KCC Setup Wizard
FinishedHeadingLabel=Completing the KCC Setup Wizard
[Registry]
Root: HKCR; SubKey: ".cbz"; ValueType: string; ValueData: "KCCZIP"; Flags: uninsdeletekey; Tasks: CBZassociation
Root: HKCR; SubKey: "KCCZIP"; ValueType: string; ValueData: "KCC ZIP Archive"; Flags: uninsdeletekey; Tasks: CBZassociation
Root: HKCR; SubKey: "KCCZIP\Shell\Open\Command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: CBZassociation
Root: HKCR; Subkey: "KCCZIP\DefaultIcon"; ValueType: string; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletevalue; Tasks: CBZassociation
Root: HKCR; SubKey: ".cbr"; ValueType: string; ValueData: "KCCRAR"; Flags: uninsdeletekey; Tasks: CBRassociation
Root: HKCR; SubKey: "KCCRAR"; ValueType: string; ValueData: "KCC RAR Archive"; Flags: uninsdeletekey; Tasks: CBRassociation
Root: HKCR; SubKey: "KCCRAR\Shell\Open\Command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: CBRassociation
Root: HKCR; Subkey: "KCCRAR\DefaultIcon"; ValueType: string; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletevalue; Tasks: CBRassociation
Root: HKCR; SubKey: ".cb7"; ValueType: string; ValueData: "KCCCB7"; Flags: uninsdeletekey; Tasks: CB7association
Root: HKCR; SubKey: "KCCCB7"; ValueType: string; ValueData: "KCC 7z Archive"; Flags: uninsdeletekey; Tasks: CB7association
Root: HKCR; SubKey: "KCCCB7\Shell\Open\Command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey; Tasks: CB7association
Root: HKCR; Subkey: "KCCCB7\DefaultIcon"; ValueType: string; ValueData: "{app}\{#MyAppExeName},0"; Flags: uninsdeletevalue; Tasks: CB7association
[Code]
function GetUninstallString(): String;
var
sUnInstPath: String;
sUnInstallString: String;
begin
sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1');
sUnInstallString := '';
if not RegQueryStringValue(HKLM, sUnInstPath, 'UninstallString', sUnInstallString) then
RegQueryStringValue(HKCU, sUnInstPath, 'UninstallString', sUnInstallString);
Result := sUnInstallString;
end;
function IsUpgrade(): Boolean;
begin
Result := (GetUninstallString() <> '');
end;
function UnInstallOldVersion(): Integer;
var
sUnInstallString: String;
iResultCode: Integer;
begin
Result := 0;
sUnInstallString := GetUninstallString();
if sUnInstallString <> '' then begin
sUnInstallString := RemoveQuotes(sUnInstallString);
if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then
Result := 3
else
Result := 2;
end else
Result := 1;
end;
procedure CurStepChanged(CurStep: TSetupStep);
begin
if (CurStep=ssInstall) then
begin
if (IsUpgrade()) then
begin
UnInstallOldVersion();
end;
end;
end;

105
kcc.py
View File

@@ -18,76 +18,61 @@
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
import os
import sys
import platform
from pathlib import Path
if sys.version_info < (3, 8, 0):
print('ERROR: This is a Python 3.8+ script!')
sys.exit(1)
def modify_path():
if platform.system() == 'Darwin':
mac_paths = [
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
[
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif platform.system() == 'Linux':
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(
[
str(Path.home() / ".local" / "bin"),
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif platform.system() == 'Windows':
win_paths = [
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
os.path.expandvars('%UserProfile%\\Kindle Previewer 3\\lib\\fc\\bin\\'),
'C:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'D:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'E:\\Apps\\Kindle Previewer 3\\lib\\fc\\bin',
'C:\\Program Files\\7-Zip',
'D:\\Program Files\\7-Zip',
'E:\\Program Files\\7-Zip',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
exit(1)
# OS specific workarounds
import os
if sys.platform.startswith('darwin'):
# prioritize KC2 since it optionally also installs KP3
mac_paths = [
'/Applications/Kindle Comic Creator/Kindle Comic Creator.app/Contents/MacOS',
'/Applications/Kindle Previewer 3.app/Contents/lib/fc/bin/',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths +
[
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
]
)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(mac_paths)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
elif sys.platform.startswith('win'):
# prioritize KC2 since it optionally also installs KP3
win_paths = [
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\KC2'),
os.path.expandvars('%LOCALAPPDATA%\\Amazon\\Kindle Previewer 3\\lib\\fc\\bin\\'),
'C:\\Program Files\\7-Zip',
]
if getattr(sys, 'frozen', False):
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths)
os.chdir(os.path.dirname(os.path.abspath(sys.executable)))
else:
os.environ['PATH'] += os.pathsep + os.pathsep.join(win_paths +
[
os.path.dirname(os.path.abspath(__file__)) + '/other/windows/',
]
)
os.chdir(os.path.dirname(os.path.abspath(__file__)))
# Load additional Sentry configuration
# if getattr(sys, 'frozen', False):
# try:
# import kindlecomicconverter.sentry
# except ImportError:
# pass
from multiprocessing import freeze_support, set_start_method
from kindlecomicconverter.startup import start
if __name__ == "__main__":
modify_path()
set_start_method('spawn')
freeze_support()
start()

View File

@@ -8,10 +8,10 @@ a = Analysis(['kcc.py'],
pathex=['.'],
binaries=[],
datas=[],
hiddenimports=['_cffi_backend'],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=['pkg_resources'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
################################################################################
## Form generated from reading UI file 'MetaEditor.ui'
##
## Created by: Qt User Interface Compiler version 6.9.3
## Created by: Qt User Interface Compiler version 6.5.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
@@ -27,7 +27,7 @@ class Ui_editorDialog(object):
editorDialog.resize(400, 260)
editorDialog.setMinimumSize(QSize(400, 260))
icon = QIcon()
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon.addFile(u":/Icon/icons/comic2ebook.png", QSize(), QIcon.Normal, QIcon.Off)
editorDialog.setWindowIcon(icon)
self.verticalLayout = QVBoxLayout(editorDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
@@ -60,62 +60,52 @@ class Ui_editorDialog(object):
self.label_3 = QLabel(self.editorWidget)
self.label_3.setObjectName(u"label_3")
self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1)
self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
self.numberLine = QLineEdit(self.editorWidget)
self.numberLine.setObjectName(u"numberLine")
self.gridLayout.addWidget(self.numberLine, 3, 1, 1, 1)
self.gridLayout.addWidget(self.numberLine, 2, 1, 1, 1)
self.label_4 = QLabel(self.editorWidget)
self.label_4.setObjectName(u"label_4")
self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1)
self.gridLayout.addWidget(self.label_4, 3, 0, 1, 1)
self.writerLine = QLineEdit(self.editorWidget)
self.writerLine.setObjectName(u"writerLine")
self.gridLayout.addWidget(self.writerLine, 4, 1, 1, 1)
self.gridLayout.addWidget(self.writerLine, 3, 1, 1, 1)
self.label_5 = QLabel(self.editorWidget)
self.label_5.setObjectName(u"label_5")
self.gridLayout.addWidget(self.label_5, 5, 0, 1, 1)
self.gridLayout.addWidget(self.label_5, 4, 0, 1, 1)
self.pencillerLine = QLineEdit(self.editorWidget)
self.pencillerLine.setObjectName(u"pencillerLine")
self.gridLayout.addWidget(self.pencillerLine, 5, 1, 1, 1)
self.gridLayout.addWidget(self.pencillerLine, 4, 1, 1, 1)
self.label_6 = QLabel(self.editorWidget)
self.label_6.setObjectName(u"label_6")
self.gridLayout.addWidget(self.label_6, 6, 0, 1, 1)
self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1)
self.inkerLine = QLineEdit(self.editorWidget)
self.inkerLine.setObjectName(u"inkerLine")
self.gridLayout.addWidget(self.inkerLine, 6, 1, 1, 1)
self.gridLayout.addWidget(self.inkerLine, 5, 1, 1, 1)
self.label_7 = QLabel(self.editorWidget)
self.label_7.setObjectName(u"label_7")
self.gridLayout.addWidget(self.label_7, 7, 0, 1, 1)
self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1)
self.coloristLine = QLineEdit(self.editorWidget)
self.coloristLine.setObjectName(u"coloristLine")
self.gridLayout.addWidget(self.coloristLine, 7, 1, 1, 1)
self.label_8 = QLabel(self.editorWidget)
self.label_8.setObjectName(u"label_8")
self.gridLayout.addWidget(self.label_8, 2, 0, 1, 1)
self.titleLine = QLineEdit(self.editorWidget)
self.titleLine.setObjectName(u"titleLine")
self.gridLayout.addWidget(self.titleLine, 2, 1, 1, 1)
self.gridLayout.addWidget(self.coloristLine, 6, 1, 1, 1)
self.verticalLayout.addWidget(self.editorWidget)
@@ -127,7 +117,7 @@ class Ui_editorDialog(object):
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.statusLabel = QLabel(self.optionWidget)
self.statusLabel.setObjectName(u"statusLabel")
sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding)
sizePolicy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.statusLabel.sizePolicy().hasHeightForWidth())
@@ -139,7 +129,7 @@ class Ui_editorDialog(object):
self.okButton.setObjectName(u"okButton")
self.okButton.setMinimumSize(QSize(0, 30))
icon1 = QIcon()
icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon1.addFile(u":/Other/icons/convert.png", QSize(), QIcon.Normal, QIcon.Off)
self.okButton.setIcon(icon1)
self.horizontalLayout.addWidget(self.okButton)
@@ -148,7 +138,7 @@ class Ui_editorDialog(object):
self.cancelButton.setObjectName(u"cancelButton")
self.cancelButton.setMinimumSize(QSize(0, 30))
icon2 = QIcon()
icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
icon2.addFile(u":/Other/icons/clear.png", QSize(), QIcon.Normal, QIcon.Off)
self.cancelButton.setIcon(icon2)
self.horizontalLayout.addWidget(self.cancelButton)
@@ -156,15 +146,6 @@ class Ui_editorDialog(object):
self.verticalLayout.addWidget(self.optionWidget)
QWidget.setTabOrder(self.seriesLine, self.volumeLine)
QWidget.setTabOrder(self.volumeLine, self.titleLine)
QWidget.setTabOrder(self.titleLine, self.numberLine)
QWidget.setTabOrder(self.numberLine, self.writerLine)
QWidget.setTabOrder(self.writerLine, self.pencillerLine)
QWidget.setTabOrder(self.pencillerLine, self.inkerLine)
QWidget.setTabOrder(self.inkerLine, self.coloristLine)
QWidget.setTabOrder(self.coloristLine, self.okButton)
QWidget.setTabOrder(self.okButton, self.cancelButton)
self.retranslateUi(editorDialog)
@@ -180,7 +161,6 @@ class Ui_editorDialog(object):
self.label_5.setText(QCoreApplication.translate("editorDialog", u"Penciller:", None))
self.label_6.setText(QCoreApplication.translate("editorDialog", u"Inker:", None))
self.label_7.setText(QCoreApplication.translate("editorDialog", u"Colorist:", None))
self.label_8.setText(QCoreApplication.translate("editorDialog", u"Title:", None))
self.statusLabel.setText("")
self.okButton.setText(QCoreApplication.translate("editorDialog", u"Save", None))
self.cancelButton.setText(QCoreApplication.translate("editorDialog", u"Cancel", None))

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -18,17 +18,13 @@
# PERFORMANCE OF THIS SOFTWARE.
#
import math
import os
import sys
from argparse import ArgumentParser
from shutil import rmtree
from shutil import rmtree, copytree, move
from multiprocessing import Pool
from PIL import Image, ImageChops, ImageOps, ImageDraw, ImageFilter, ImageFile
from PIL.Image import Dither
from .shared import dot_clean, getImageFileName, walkLevel, walkSort, sanitizeTrace
ImageFile.LOAD_TRUNCATED_IMAGES = True
from PIL import Image, ImageChops, ImageOps, ImageDraw
from .shared import getImageFileName, walkLevel, walkSort, sanitizeTrace
def mergeDirectoryTick(output):
@@ -48,7 +44,6 @@ def mergeDirectory(work):
imagesValid = []
sizes = []
targetHeight = 0
dot_clean(directory)
for root, _, files in walkLevel(directory, 0):
for name in files:
if getImageFileName(name) is not None:
@@ -62,19 +57,18 @@ def mergeDirectory(work):
imagesValid.append(i[0])
# Silently drop directories that contain too many images
# 131072 = GIMP_MAX_IMAGE_SIZE / 4
if targetHeight > 131072 * 4:
raise RuntimeError(f'Image too tall at {targetHeight} pixels. {targetWidth} pixels wide. Try using separate chapter folders or file fusion.')
if targetHeight > 131072:
return None
result = Image.new('RGB', (targetWidth, targetHeight))
y = 0
for i in imagesValid:
with Image.open(i) as img:
img = img.convert('RGB')
if img.size[0] < targetWidth or img.size[0] > targetWidth:
widthPercent = (targetWidth / float(img.size[0]))
heightSize = int((float(img.size[1]) * float(widthPercent)))
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
result.paste(img, (0, y))
y += img.size[1]
img = Image.open(i).convert('RGB')
if img.size[0] < targetWidth or img.size[0] > targetWidth:
widthPercent = (targetWidth / float(img.size[0]))
heightSize = int((float(img.size[1]) * float(widthPercent)))
img = ImageOps.fit(img, (targetWidth, heightSize), method=Image.BICUBIC, centering=(0.5, 0.5))
result.paste(img, (0, y))
y += img.size[1]
os.remove(i)
savePath = os.path.split(imagesValid[0])
result.save(os.path.join(savePath[0], os.path.splitext(savePath[1])[0] + '.png'), 'PNG')
@@ -106,11 +100,7 @@ def splitImage(work):
Image.warnings.simplefilter('error', Image.DecompressionBombWarning)
Image.MAX_IMAGE_PIXELS = 1000000000
imgOrg = Image.open(filePath).convert('RGB')
# I experimented with custom vertical edge kernel [-1, 2, -1] but got poor results
imgEdges = Image.open(filePath).convert('L').filter(ImageFilter.FIND_EDGES)
# threshold of 8 is too high. 5 is too low.
imgProcess = imgEdges.point(lambda p: 255 if p > 6 else 0).convert('1', dither=Dither.NONE)
imgProcess = Image.open(filePath).convert('1')
widthImg, heightImg = imgOrg.size
if heightImg > opt.height:
if opt.debug:
@@ -121,71 +111,47 @@ def splitImage(work):
yWork = 0
panelDetected = False
panels = []
# check git history for how these constant values changed
h_pad = int(widthImg / 20)
v_pad = int(widthImg / 80)
if v_pad % 2:
v_pad += 1
while yWork < heightImg:
tmpImg = imgProcess.crop((h_pad, yWork, widthImg - h_pad, yWork + v_pad))
tmpImg = imgProcess.crop((4, yWork, widthImg-4, yWork + 4))
solid = detectSolid(tmpImg)
if not solid and not panelDetected:
panelDetected = True
panelY1 = yWork
if heightImg - yWork <= (v_pad // 2):
panelY1 = yWork - 2
if heightImg - yWork <= 5:
if not solid and panelDetected:
panelY2 = heightImg
panelDetected = False
panels.append((panelY1, panelY2, panelY2 - panelY1))
if solid and panelDetected:
panelDetected = False
panelY2 = yWork
# skip short panel at start
if panelY1 < v_pad * 2 and panelY2 - panelY1 < v_pad * 2:
continue
panelY2 = yWork + 6
panels.append((panelY1, panelY2, panelY2 - panelY1))
yWork += v_pad // 2
max_width = 1072
virtual_width = min((max_width, opt.width, widthImg))
if opt.width > max_width:
virtual_height = int(opt.height/max_width*virtual_width)
else:
virtual_height = int(opt.height/opt.width*virtual_width)
opt.height = virtual_height
yWork += 5
# Split too big panels
panelsProcessed = []
for panel in panels:
# 1.52 too high
if panel[2] <= opt.height * 1.5:
panelsProcessed.append(panel)
elif panel[2] <= opt.height * 2:
elif panel[2] < opt.height * 2:
diff = panel[2] - opt.height
panelsProcessed.append((panel[0], panel[1] - diff, opt.height))
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
else:
# split super long panels with overlap
parts = math.ceil(panel[2] / opt.height)
parts = round(panel[2] / opt.height)
diff = panel[2] // parts
panelsProcessed.append((panel[0], panel[0] + opt.height, opt.height))
for x in range(1, parts - 1):
start = panel[0] + (x * diff)
panelsProcessed.append((start, start + opt.height, opt.height))
panelsProcessed.append((panel[1] - opt.height, panel[1], opt.height))
for x in range(0, parts):
panelsProcessed.append((panel[0] + (x * diff), panel[1] - ((parts - x - 1) * diff), diff))
if opt.debug:
for panel in panelsProcessed:
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.show()
debugImage.save(os.path.join(path, os.path.splitext(name)[0] + '-debug.png'), 'PNG')
# Create virtual pages
pages = []
currentPage = []
# TODO: 1.25 way too high, 1.1 too high, 1.05 slightly too high(?), optimized for 2 page landscape reading
# opt.height = max_height = virtual_height * 1.00
pageLeft = opt.height
panelNumber = 0
for panel in panelsProcessed:
@@ -215,14 +181,14 @@ def splitImage(work):
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).zfill(4) + '.png'), 'PNG')
newPage.save(os.path.join(path, os.path.splitext(name)[0] + '-' + str(pageNumber) + '.png'), 'PNG')
pageNumber += 1
os.remove(filePath)
except Exception:
return str(sys.exc_info()[1]), sanitizeTrace(sys.exc_info()[2])
def main(argv=None, job_progress='', qtgui=None):
def main(argv=None, qtgui=None):
global args, GUI, splitWorkerPool, splitWorkerOutput, mergeWorkerPool, mergeWorkerOutput
parser = ArgumentParser(prog="kcc-c2p", usage="kcc-c2p [options] [input]", add_help=False)
@@ -234,8 +200,6 @@ def main(argv=None, job_progress='', qtgui=None):
" 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("-x", "--width", type=int, dest="width", default=0,
help="Width 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,
@@ -254,14 +218,16 @@ def main(argv=None, job_progress='', qtgui=None):
return 1
if args.height > 0:
for sourceDir in args.input:
targetDir = sourceDir
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(f"{job_progress}Merging images...")
print("Merging images...")
directoryNumer = 1
mergeWork = []
mergeWorkerOutput = []
@@ -273,7 +239,7 @@ def main(argv=None, job_progress='', qtgui=None):
directoryNumer += 1
mergeWork.append([os.path.join(root, directory)])
if GUI:
GUI.progressBarTick.emit(f'{job_progress}Combining images')
GUI.progressBarTick.emit('Combining images')
GUI.progressBarTick.emit(str(directoryNumer))
for i in mergeWork:
mergeWorkerPool.apply_async(func=mergeDirectory, args=(i, ), callback=mergeDirectoryTick)
@@ -286,8 +252,7 @@ def main(argv=None, job_progress='', qtgui=None):
rmtree(targetDir, True)
raise RuntimeError("One of workers crashed. Cause: " + mergeWorkerOutput[0][0],
mergeWorkerOutput[0][1])
print(f"{job_progress}Splitting images...")
dot_clean(targetDir)
print("Splitting images...")
for root, _, files in os.walk(targetDir, False):
for name in files:
if getImageFileName(name) is not None:
@@ -296,7 +261,7 @@ def main(argv=None, job_progress='', qtgui=None):
else:
os.remove(os.path.join(root, name))
if GUI:
GUI.progressBarTick.emit(f'{job_progress}Splitting images')
GUI.progressBarTick.emit('Splitting images')
GUI.progressBarTick.emit(str(pagenumber))
GUI.progressBarTick.emit('tick')
if len(work) > 0:
@@ -304,7 +269,6 @@ def main(argv=None, job_progress='', qtgui=None):
splitWorkerPool.apply_async(func=splitImage, args=(i, ), callback=splitImageTick)
splitWorkerPool.close()
splitWorkerPool.join()
dot_clean(targetDir)
if GUI and not GUI.conversionAlive:
rmtree(targetDir, True)
raise UserWarning("Conversion interrupted.")
@@ -312,9 +276,12 @@ def main(argv=None, job_progress='', qtgui=None):
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("C2P: Source directory is empty.")
raise UserWarning("Source directory is empty.")
else:
raise UserWarning("Provided input is not a directory.")
else:

View File

@@ -18,119 +18,87 @@
# PERFORMANCE OF THIS SOFTWARE.
#
from functools import cached_property, lru_cache
import os
from pathlib import Path
import platform
import subprocess
import distro
from subprocess import STDOUT, PIPE, CalledProcessError
from psutil import Popen
from shutil import move
from subprocess import STDOUT, PIPE
from xml.dom.minidom import parseString
from xml.parsers.expat import ExpatError
from .shared import IMAGE_TYPES, subprocess_run
EXTRACTION_ERROR = 'Failed to extract archive. Try extracting file outside of KCC.'
SEVENZIP = '7zz' if platform.system() == 'Darwin' else '7z'
TAR = 'bsdtar' if platform.system() == 'Linux' else 'tar'
class ComicArchive:
def __init__(self, filepath):
self.filepath = filepath
self.type = None
if not os.path.isfile(self.filepath):
raise OSError('File not found.')
self.dirname, self.basename = os.path.split(filepath)
@cached_property
def type(self):
extraction_commands = [
[SEVENZIP, 'l', '-y', '-p1', self.basename],
]
if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append(
['unrar', 'l', '-y', '-p1', self.basename],
)
for cmd in extraction_commands:
try:
process = subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
for line in process.stdout.splitlines():
if b'Type =' in line:
return line.rstrip().decode().split(' = ')[1].upper()
except FileNotFoundError:
pass
except CalledProcessError:
pass
raise OSError(EXTRACTION_ERROR)
process = Popen('7z l -y -p1 "' + self.filepath + '"', stderr=STDOUT, stdout=PIPE, stdin=PIPE, shell=True)
for line in process.stdout:
if b'Type =' in line:
self.type = line.rstrip().decode().split(' = ')[1].upper()
break
process.communicate()
if process.returncode != 0 and distro.id() == 'fedora':
process = Popen('unrar l -y -p1 "' + self.filepath + '"', stderr=STDOUT, stdout=PIPE, stdin=PIPE, shell=True)
for line in process.stdout:
if b'Details: ' in line:
self.type = line.rstrip().decode().split(' ')[1].upper()
break
process.communicate()
if process.returncode != 0:
raise OSError('Archive is corrupted or encrypted.')
elif self.type not in ['7Z', 'RAR', 'RAR5', 'ZIP']:
raise OSError('Unsupported archive format.')
elif self.type not in ['7Z', 'RAR', 'RAR5', 'ZIP']:
raise OSError('Unsupported archive format.')
def extract(self, targetdir):
if not os.path.isdir(targetdir):
raise OSError('Target directory doesn\'t exist.')
if Path(self.basename).suffix.lower() in IMAGE_TYPES:
raise UserWarning('Put images into folder and drag and drop folder into KCC window.')
missing = []
extraction_commands = [
[TAR, '--exclude', '__MACOSX', '--exclude', '.DS_Store', '--exclude', 'thumbs.db', '--exclude', 'Thumbs.db', '-xf', self.basename, '-C', targetdir],
[SEVENZIP, 'x', '-y', '-xr!__MACOSX', '-xr!.DS_Store', '-xr!thumbs.db', '-xr!Thumbs.db', '-o' + targetdir, self.basename],
]
if platform.system() == 'Darwin':
extraction_commands.append(
['unar', self.basename, '-D', '-f', '-o', targetdir]
)
extraction_commands.reverse()
if distro.id() == 'fedora' or distro.like() == 'fedora':
extraction_commands.append(
['unrar', 'x', '-y', '-x__MACOSX', '-x.DS_Store', '-xthumbs.db', '-xThumbs.db', self.basename, targetdir]
)
for cmd in extraction_commands:
try:
subprocess_run(cmd, capture_output=True, check=True, cwd=self.dirname)
return targetdir
except FileNotFoundError:
missing.append(cmd[0])
except CalledProcessError:
pass
if missing:
raise OSError(f'Extraction failed, install <a href="https://github.com/ciromattia/kcc#7-zip">specialized extraction software.</a> ')
else:
raise OSError(EXTRACTION_ERROR)
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()
if process.returncode != 0 and distro.id() == 'fedora':
process = Popen('unrar x -y -x__MACOSX -x.DS_Store -xthumbs.db -xThumbs.db "' + self.filepath + '" "' +
targetdir + '"', stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
process.communicate()
if process.returncode != 0:
raise OSError('Failed to extract archive.')
elif process.returncode != 0 and platform.system() == 'Darwin':
process = subprocess.run(f"unar '{self.filepath}' -f -o '{targetdir}'",
stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
if process.returncode != 0:
raise Exception(process.stdout.decode("utf-8"))
elif process.returncode != 0:
raise OSError('Failed to extract archive. Check if p7zip-rar is installed.')
tdir = os.listdir(targetdir)
if 'ComicInfo.xml' in tdir:
tdir.remove('ComicInfo.xml')
if len(tdir) == 1 and os.path.isdir(os.path.join(targetdir, tdir[0])):
for f in os.listdir(os.path.join(targetdir, tdir[0])):
move(os.path.join(targetdir, tdir[0], f), targetdir)
os.rmdir(os.path.join(targetdir, tdir[0]))
return targetdir
def addFile(self, sourcefile):
if self.type in ['RAR', 'RAR5']:
raise NotImplementedError
process = subprocess_run([SEVENZIP, 'a', '-y', self.basename, sourcefile],
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
process = Popen('7z a -y "' + self.filepath + '" "' + sourcefile + '"',
stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
process.communicate()
if process.returncode != 0:
raise OSError('Failed to add the file.')
def extractMetadata(self):
process = subprocess_run([SEVENZIP, 'x', '-y', '-so', self.basename, 'ComicInfo.xml'],
stdout=PIPE, stderr=STDOUT, cwd=self.dirname)
process = Popen('7z x -y -so "' + self.filepath + '" ComicInfo.xml',
stdout=PIPE, stderr=STDOUT, stdin=PIPE, shell=True)
xml = process.communicate()
if process.returncode != 0:
raise OSError(EXTRACTION_ERROR)
raise OSError('Failed to extract archive.')
try:
return parseString(process.stdout)
return parseString(xml[0])
except ExpatError:
return None
@lru_cache
def available_archive_tools():
available = []
for tool in [TAR, SEVENZIP, 'unar', 'unrar']:
try:
subprocess_run([tool], stdout=PIPE, stderr=STDOUT)
available.append(tool)
except (FileNotFoundError, CalledProcessError):
pass
return available

View File

@@ -1,28 +0,0 @@
def threshold_from_power(power):
return 240-(power*64)
'''
Groups close values together
'''
def group_close_values(vals, max_dist_tolerated):
groups = []
group_start = -1
group_end = 0
for i in range(len(vals)):
dist = vals[i] - group_end
if group_start == -1:
group_start = vals[i]
group_end = vals[i]
elif dist <= max_dist_tolerated:
group_end = vals[i]
else:
groups.append((group_start, group_end))
group_start = -1
group_end = -1
if group_start != -1:
groups.append((group_start, group_end))
return groups

View File

@@ -136,11 +136,7 @@ def del_exth(rec0, exth_num):
class DualMobiMetaFix:
def __init__(self, infile, outfile, asin, is_pdoc):
cdetype = b'EBOK'
if is_pdoc:
cdetype = b'PDOC'
def __init__(self, infile, outfile, asin):
shutil.copyfile(infile, outfile)
f = open(outfile, "r+b")
self.datain = mmap.mmap(f.fileno(), 0)
@@ -151,7 +147,7 @@ class DualMobiMetaFix:
rec0 = self.datain_rec0
rec0 = del_exth(rec0, 501)
rec0 = del_exth(rec0, 113)
rec0 = add_exth(rec0, 501, cdetype)
rec0 = add_exth(rec0, 501, b'EBOK')
rec0 = add_exth(rec0, 113, asin)
replacesection(self.datain, 0, rec0)
@@ -178,7 +174,7 @@ class DualMobiMetaFix:
rec0 = self.datain_kfrec0
rec0 = del_exth(rec0, 501)
rec0 = del_exth(rec0, 113)
rec0 = add_exth(rec0, 501, cdetype)
rec0 = add_exth(rec0, 501, b'EBOK')
rec0 = add_exth(rec0, 113, asin)
replacesection(self.datain, datain_kf8, rec0)

View File

@@ -20,18 +20,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import os
import numpy as np
from pathlib import Path
from functools import cached_property
import mozjpeg_lossless_optimization
from PIL import Image, ImageOps, ImageFile, ImageChops, ImageDraw
from PIL import Image, ImageOps, ImageStat, ImageChops, ImageFilter
from .shared import md5Checksum
from .rainbow_artifacts_eraser import erase_rainbow_artifacts
from .page_number_crop_alg import get_bbox_crop_margin_page_number, get_bbox_crop_margin
from .inter_panel_crop_alg import crop_empty_inter_panel
AUTO_CROP_THRESHOLD = 0.015
ImageFile.LOAD_TRUNCATED_IMAGES = True
# 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:
@@ -85,66 +83,32 @@ class ProfileData:
PalleteNull = [
]
ProfilesKindleEBOK = {
}
ProfilesKindlePDOC = {
'K1': ("Kindle 1", (600, 670), Palette4, 1.0),
'K2': ("Kindle 2", (600, 670), Palette15, 1.0),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.0),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.0),
'K57': ("Kindle 5/7", (600, 800), Palette16, 1.0),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.0),
'KV': ("Kindle Voyage", (1072, 1448), Palette16, 1.0),
'KPW34': ("Kindle Paperwhite 3/4/Oasis", (1072, 1448), Palette16, 1.0),
'K810': ("Kindle 8/10", (600, 800), Palette16, 1.0),
'KO': ("Kindle Oasis 2/3/Paperwhite 12", (1264, 1680), Palette16, 1.0),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.0),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.0),
'KS1860': ("Kindle 1860", (1860, 1920), Palette16, 1.0),
'KS1920': ("Kindle 1920", (1920, 1920), Palette16, 1.0),
'KS1240': ("Kindle 1240", (1240, 1860), Palette16, 1.0),
'KS': ("Kindle Scribe 1/2", (1860, 2480), Palette16, 1.0),
'KCS': ("Kindle Colorsoft", (1264, 1680), Palette16, 1.0),
'KS3': ("Kindle Scribe 3", (1986, 2648), Palette16, 1.0),
'KSCS': ("Kindle Scribe Colorsoft", (1986, 2648), Palette16, 1.0),
}
ProfilesKindle = {
**ProfilesKindleEBOK,
**ProfilesKindlePDOC
}
ProfilesKobo = {
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.0),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.0),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.0),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.0),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.0),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.0),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.0),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.0),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.0),
'KoCC': ("Kobo Clara Colour", (1072, 1448), Palette16, 1.0),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.0),
'KoLC': ("Kobo Libra Colour", (1264, 1680), Palette16, 1.0),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.0),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.0),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.0),
}
ProfilesRemarkable = {
'Rmk1': ("reMarkable 1", (1404, 1872), Palette16, 1.0),
'Rmk2': ("reMarkable 2", (1404, 1872), Palette16, 1.0),
'RmkPP': ("reMarkable Paper Pro", (1620, 2160), Palette16, 1.0),
'RmkPPMove': ("reMarkable Paper Pro Move", (954, 1696), Palette16, 1.0),
}
Profiles = {
**ProfilesKindle,
**ProfilesKobo,
**ProfilesRemarkable,
'OTHER': ("Other", (0, 0), Palette16, 1.0),
'K1': ("Kindle 1", (600, 670), Palette4, 1.8),
'K11': ("Kindle 11", (1072, 1448), Palette16, 1.8),
'K2': ("Kindle 2", (600, 670), Palette15, 1.8),
'K34': ("Kindle Keyboard/Touch", (600, 800), Palette16, 1.8),
'K578': ("Kindle", (600, 800), Palette16, 1.8),
'KDX': ("Kindle DX/DXG", (824, 1000), Palette16, 1.8),
'KPW': ("Kindle Paperwhite 1/2", (758, 1024), Palette16, 1.8),
'KV': ("Kindle Paperwhite 3/4/Voyage/Oasis", (1072, 1448), Palette16, 1.8),
'KPW5': ("Kindle Paperwhite 5/Signature Edition", (1236, 1648), Palette16, 1.8),
'KO': ("Kindle Oasis 2/3", (1264, 1680), Palette16, 1.8),
'KS': ("Kindle Scribe", (1860, 2480), Palette16, 1.8),
'KoMT': ("Kobo Mini/Touch", (600, 800), Palette16, 1.8),
'KoG': ("Kobo Glo", (768, 1024), Palette16, 1.8),
'KoGHD': ("Kobo Glo HD", (1072, 1448), Palette16, 1.8),
'KoA': ("Kobo Aura", (758, 1024), Palette16, 1.8),
'KoAHD': ("Kobo Aura HD", (1080, 1440), Palette16, 1.8),
'KoAH2O': ("Kobo Aura H2O", (1080, 1430), Palette16, 1.8),
'KoAO': ("Kobo Aura ONE", (1404, 1872), Palette16, 1.8),
'KoN': ("Kobo Nia", (758, 1024), Palette16, 1.8),
'KoC': ("Kobo Clara HD/Kobo Clara 2E", (1072, 1448), Palette16, 1.8),
'KoL': ("Kobo Libra H2O/Kobo Libra 2", (1264, 1680), Palette16, 1.8),
'KoF': ("Kobo Forma", (1440, 1920), Palette16, 1.8),
'KoS': ("Kobo Sage", (1440, 1920), Palette16, 1.8),
'KoE': ("Kobo Elipsa", (1404, 1872), Palette16, 1.8),
'OTHER': ("Other", (0, 0), Palette16, 1.8),
}
@@ -155,13 +119,8 @@ class ComicPageParser:
self.source = source
self.size = self.opt.profileData[1]
self.payload = []
# Detect corruption in source image, let caller catch any exceptions triggered.
srcImgPath = os.path.join(source[0], source[1])
# Image.open(srcImgPath).verify()
with Image.open(srcImgPath) as im:
self.image = im.copy()
self.image = Image.open(os.path.join(source[0], source[1])).convert('RGB')
self.color = self.colorCheck()
self.fill = self.fillCheck()
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -192,13 +151,10 @@ class ComicPageParser:
new_image = Image.new("RGB", (int(width / 2), int(height*2)))
new_image.paste(pageone, (0, 0))
new_image.paste(pagetwo, (0, height))
self.payload.append(['N', self.source, new_image, self.fill])
self.payload.append(['N', self.source, new_image, self.color, self.fill])
elif (width > height) != (dstwidth > dstheight) and width <= dstheight and height <= dstwidth \
and not self.opt.webtoon and self.opt.splitter == 1:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True), self.color, self.fill])
elif (width > height) != (dstwidth > dstheight) and not self.opt.webtoon:
if self.opt.splitter != 1:
if width > height:
@@ -213,15 +169,35 @@ class ComicPageParser:
else:
pageone = self.image.crop(leftbox)
pagetwo = self.image.crop(rightbox)
self.payload.append(['S1', self.source, pageone, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.fill])
self.payload.append(['S1', self.source, pageone, self.color, self.fill])
self.payload.append(['S2', self.source, pagetwo, self.color, self.fill])
if self.opt.splitter > 0:
spread = self.image
if not self.opt.norotate:
spread = spread.rotate(90, Image.Resampling.BICUBIC, True)
self.payload.append(['R', self.source, spread, self.fill])
self.payload.append(['R', self.source, self.image.rotate(90, Image.Resampling.BICUBIC, True),
self.color, self.fill])
else:
self.payload.append(['N', self.source, self.image, self.fill])
self.payload.append(['N', self.source, self.image, self.color, self.fill])
def colorCheck(self):
if self.opt.webtoon:
return True
else:
img = self.image.copy()
bands = img.getbands()
if bands == ('R', 'G', 'B') or bands == ('R', 'G', 'B', 'A'):
thumb = img.resize((40, 40))
SSE, bias = 0, [0, 0, 0]
bias = ImageStat.Stat(thumb).mean[:3]
bias = [b - sum(bias) / 3 for b in bias]
for pixel in thumb.getdata():
mu = sum(pixel) / 3
SSE += sum((pixel[i] - mu - bias[i]) * (pixel[i] - mu - bias[i]) for i in [0, 1, 2])
MSE = float(SSE) / (40 * 40)
if MSE > 22:
return True
else:
return False
else:
return False
def fillCheck(self):
if self.opt.bordersColor:
@@ -263,295 +239,159 @@ class ComicPageParser:
class ComicPage:
def __init__(self, options, mode, path, image, fill):
def __init__(self, options, mode, path, image, color, fill):
self.opt = options
_, self.size, self.palette, self.gamma = self.opt.profileData
if self.opt.hq:
self.size = (int(self.size[0] * 1.5), int(self.size[1] * 1.5))
self.original_color_mode = image.mode
# TODO: color check earlier
self.image = image.convert("RGB")
self.image = image
self.color = color
self.fill = fill
self.rotated = False
self.orgPath = os.path.join(path[0], path[1])
self.targetPathStart = os.path.join(path[0], os.path.splitext(path[1])[0])
if 'N' in mode:
self.targetPathOrder = '-kcc-x'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC'
elif 'R' in mode:
self.targetPathOrder = '-kcc-a' if options.rotatefirst else '-kcc-d'
if not options.norotate:
self.rotated = True
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-A'
self.rotated = True
elif 'S1' in mode:
self.targetPathOrder = '-kcc-b'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-B'
elif 'S2' in mode:
self.targetPathOrder = '-kcc-c'
self.targetPath = os.path.join(path[0], os.path.splitext(path[1])[0]) + '-KCC-C'
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
Image.Resampling = Image
@cached_property
def color(self):
if self.original_color_mode in ("L", "1"):
return False
if self.opt.webtoon:
return True
if self.calculate_color():
return True
return False
# cut off pixels from both ends of the histogram to remove jpg compression artifacts
# for better accuracy, you could split the image in half and analyze each half separately
def histograms_cutoff(self, cb_hist, cr_hist, cutoff=(2, 2)):
if cutoff == (0, 0):
return cb_hist, cr_hist
for h in cb_hist, cr_hist:
# get number of pixels
n = sum(h)
# remove cutoff% pixels from the low end
cut = int(n * cutoff[0] // 100)
for lo in range(256):
if cut > h[lo]:
cut = cut - h[lo]
h[lo] = 0
else:
h[lo] -= cut
cut = 0
if cut <= 0:
break
# remove cutoff% samples from the high end
cut = int(n * cutoff[1] // 100)
for hi in range(255, -1, -1):
if cut > h[hi]:
cut = cut - h[hi]
h[hi] = 0
else:
h[hi] -= cut
cut = 0
if cut <= 0:
break
return cb_hist, cr_hist
def color_precision(self, cb_hist_original, cr_hist_original, cutoff, diff_threshold):
cb_hist, cr_hist = self.histograms_cutoff(cb_hist_original.copy(), cr_hist_original.copy(), cutoff)
cb_nonzero = [i for i, e in enumerate(cb_hist) if e]
cr_nonzero = [i for i, e in enumerate(cr_hist) if e]
cb_spread = cb_nonzero[-1] - cb_nonzero[0]
cr_spread = cr_nonzero[-1] - cr_nonzero[0]
# bias adjustment, don't go lower than 7
SPREAD_THRESHOLD = 7
if self.opt.forcecolor:
if any([
cb_nonzero[0] > 128,
cr_nonzero[0] > 128,
cb_nonzero[-1] < 128,
cr_nonzero[-1] < 128,
]):
return True, True
elif cb_spread < SPREAD_THRESHOLD and cr_spread < SPREAD_THRESHOLD:
return True, False
DIFF_THRESHOLD = diff_threshold
if any([
cb_nonzero[0] <= 128 - DIFF_THRESHOLD,
cr_nonzero[0] <= 128 - DIFF_THRESHOLD,
cb_nonzero[-1] >= 128 + DIFF_THRESHOLD,
cr_nonzero[-1] >= 128 + DIFF_THRESHOLD,
]):
return True, True
return False, None
def calculate_color(self):
img = self.image.convert("YCbCr")
_, cb, cr = img.split()
cb_hist_original = cb.histogram()
cr_hist_original = cr.histogram()
# you can increase 22 but don't increase 10. 4 maybe can go higher
for cutoff, diff_threshold in [((0, 0), 22), ((.2, .2), 10), ((3, 3), 4)]:
done, decision = self.color_precision(cb_hist_original, cr_hist_original, cutoff, diff_threshold)
if done:
return decision
return False
def saveToDir(self):
try:
flags = []
if not self.opt.forcecolor and not self.opt.forcepng:
self.image = self.image.convert('L')
if self.rotated:
flags.append('Rotated')
if self.fill != 'white':
flags.append('BlackBackground')
if self.opt.kindle_scribe_azw3 and self.image.size[1] > 1920:
w, h = self.image.size
targetPath = self.save_with_codec(self.image.crop((0, 0, w, 1920)), self.targetPathStart + self.targetPathOrder + '-above')
self.save_with_codec(self.image.crop((0, 1920, w, h)), self.targetPathStart + self.targetPathOrder + '-below')
elif self.opt.kindle_scribe_azw3:
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder + '-whole')
if self.opt.forcepng:
self.image.info["transparency"] = None
self.targetPath += '.png'
self.image.save(self.targetPath, 'PNG', optimize=1)
else:
targetPath = self.save_with_codec(self.image, self.targetPathStart + self.targetPathOrder)
if os.path.isfile(self.orgPath):
os.remove(self.orgPath)
return [Path(targetPath).name, flags]
self.targetPath += '.jpg'
if self.opt.mozjpeg:
with io.BytesIO() as output:
self.image.save(output, format="JPEG", optimize=1, quality=85)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(self.targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
self.image.save(self.targetPath, 'JPEG', optimize=1, quality=85)
return [md5Checksum(self.targetPath), flags, self.orgPath]
except IOError as err:
raise RuntimeError('Cannot save image. ' + str(err))
def save_with_codec(self, image, targetPath):
if self.opt.forcepng:
image.info.pop('transparency', None)
if self.opt.iskindle and ('MOBI' in self.opt.format or 'EPUB' in self.opt.format):
targetPath += '.gif'
image.save(targetPath, 'GIF', optimize=1, interlace=False)
else:
targetPath += '.png'
image.save(targetPath, 'PNG', optimize=1)
else:
targetPath += '.jpg'
if self.opt.mozjpeg:
with io.BytesIO() as output:
image.save(output, format="JPEG", optimize=1, quality=self.opt.jpegquality)
input_jpeg_bytes = output.getvalue()
output_jpeg_bytes = mozjpeg_lossless_optimization.optimize(input_jpeg_bytes)
with open(targetPath, "wb") as output_jpeg_file:
output_jpeg_file.write(output_jpeg_bytes)
else:
image.save(targetPath, 'JPEG', optimize=1, quality=self.opt.jpegquality)
return targetPath
def gammaCorrectImage(self):
def autocontrastImage(self):
gamma = self.opt.gamma
if gamma < 0.1:
gamma = self.gamma
if self.gamma != 1.0 and self.color:
gamma = 1.0
if gamma == 1.0:
pass
self.image = ImageOps.autocontrast(self.image)
else:
self.image = Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma))
def autocontrastImage(self):
if self.opt.webtoon:
return
if self.opt.noautocontrast:
return
if self.color and not self.opt.colorautocontrast:
return
# if image is extremely low contrast, that was probably intentional
extrema = self.image.convert('L').getextrema()
if extrema[1] - extrema[0] < (255 - 32 * 3):
return
if self.opt.autolevel:
self.autolevelImage()
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
def autolevelImage(self):
img = self.image
if self.color:
img = self.image.convert("YCbCr")
y, cb, cr = img.split()
img = y
else:
img = img.convert('L')
h = img.histogram()
most_common_dark_pixel_count = max(h[:64])
black_point = h.index(most_common_dark_pixel_count)
bp = black_point
img = img.point(lambda p: p if p > bp else bp)
if self.color:
self.image = Image.merge(mode='YCbCr', bands=[img, cb, cr]).convert('RGB')
else:
self.image = img
def convertToGrayscale(self):
self.image = self.image.convert('L')
self.image = ImageOps.autocontrast(Image.eval(self.image, lambda a: int(255 * (a / 255.) ** gamma)))
def quantizeImage(self):
# remove all color pixels from image, since colorCheck() has some tolerance
# quantize with a small number of color pixels in a mostly b/w image can have unexpected results
self.image = self.image.convert("RGB")
colors = len(self.palette) // 3
if colors < 256:
self.palette += self.palette[:3] * (256 - colors)
palImg = Image.new('P', (1, 1))
palImg.putpalette(self.palette)
self.image = self.image.convert('L')
self.image = self.image.convert('RGB')
# Quantize is deprecated but new function call it internally anyway...
self.image = self.image.quantize(palette=palImg)
def optimizeForDisplay(self, eraserainbow, is_color):
# Erase rainbow artifacts for grayscale and color images by removing spectral frequencies that cause Moire interference with color filter array
if eraserainbow and all(dim > 1 for dim in self.image.size):
self.image = erase_rainbow_artifacts(self.image, is_color)
def resizeImage(self):
if self.opt.norotate and self.targetPathOrder in ('-kcc-a', '-kcc-d') and not self.opt.kindle_scribe_azw3:
# TODO: Kindle Scribe case
if self.opt.kindle_azw3 and any(dim > 1920 for dim in self.image.size):
self.image = ImageOps.contain(self.image, (1920, 1920), Image.Resampling.LANCZOS)
elif self.image.size[0] > self.size[0] * 2 or self.image.size[1] > self.size[1]:
self.image = ImageOps.contain(self.image, (self.size[0] * 2, self.size[1]), Image.Resampling.LANCZOS)
return
ratio_device = float(self.size[1]) / float(self.size[0])
ratio_image = float(self.image.size[1]) / float(self.image.size[0])
method = self.resize_method()
if self.opt.stretch:
self.image = self.image.resize(self.size, method)
elif method == Image.Resampling.BICUBIC and not self.opt.upscale:
pass
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=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 in ('CBZ', 'PDF') or self.opt.kfx) and not self.opt.white_borders:
elif self.opt.format == 'CBZ' or self.opt.kfx:
self.image = ImageOps.pad(self.image, self.size, method=method, color=self.fill)
else:
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]:
return Image.Resampling.BICUBIC
if self.image.size[0] <= self.size[0] and self.image.size[1] <= self.size[1]:
method = Image.Resampling.BICUBIC
else:
return Image.Resampling.LANCZOS
method = Image.Resampling.LANCZOS
return method
def getBoundingBox(self, tmptmg):
min_margin = [int(0.005 * i + 0.5) for i in tmptmg.size]
max_margin = [int(0.1 * i + 0.5) for i in tmptmg.size]
bbox = tmptmg.getbbox()
bbox = (
max(0, min(max_margin[0], bbox[0] - min_margin[0])),
max(0, min(max_margin[1], bbox[1] - min_margin[1])),
min(tmptmg.size[0],
max(tmptmg.size[0] - max_margin[0], bbox[2] + min_margin[0])),
min(tmptmg.size[1],
max(tmptmg.size[1] - max_margin[1], bbox[3] + min_margin[1])),
)
return bbox
def maybeCrop(self, box, minimum):
w, h = self.image.size
left, upper, right, lower = box
if self.opt.preservemargin:
ratio = 1 - self.opt.preservemargin / 100
box = left * ratio, upper * ratio, right + (w - right) * (1 - ratio), lower + (h - lower) * (1 - ratio)
box_area = (box[2] - box[0]) * (box[3] - box[1])
image_area = self.image.size[0] * self.image.size[1]
if (box_area / image_area) >= minimum:
self.image = self.image.crop(box)
def cropPageNumber(self, power, minimum):
bbox = get_bbox_crop_margin_page_number(self.image, power, self.fill)
if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum)
if self.fill != 'white':
tmptmg = self.image.convert(mode='L')
else:
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
tmptmg = tmptmg.point(lambda x: x and 255)
tmptmg = tmptmg.filter(ImageFilter.MinFilter(size=3))
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=5))
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
if tmptmg.getbbox():
self.maybeCrop(tmptmg.getbbox(), minimum)
def cropMargin(self, power, minimum):
bbox = get_bbox_crop_margin(self.image, power, self.fill)
if bbox:
w, h = self.image.size
left, upper, right, lower = bbox
# don't crop more than 10% of image
bbox = (min(0.1*w, left), min(0.1*h, upper), max(0.9*w, right), max(0.9*h, lower))
self.maybeCrop(bbox, minimum)
if self.fill != 'white':
tmptmg = self.image.convert(mode='L')
else:
tmptmg = ImageOps.invert(self.image.convert(mode='L'))
tmptmg = tmptmg.filter(ImageFilter.GaussianBlur(radius=3))
tmptmg = tmptmg.point(lambda x: (x >= 16 * power) and x)
if tmptmg.getbbox():
self.maybeCrop(self.getBoundingBox(tmptmg), minimum)
def cropInterPanelEmptySections(self, direction):
self.image = crop_empty_inter_panel(self.image, direction, background_color=self.fill)
class Cover:
def __init__(self, source, opt):
def __init__(self, source, target, opt, tomeid):
self.options = opt
self.source = source
self.target = target
if tomeid == 0:
self.tomeid = 1
else:
self.tomeid = tomeid
self.image = Image.open(source)
# backwards compatibility for Pillow >9.1.0
if not hasattr(Image, 'Resampling'):
@@ -560,59 +400,22 @@ class Cover:
def process(self):
self.image = self.image.convert('RGB')
self.image = ImageOps.autocontrast(self.image, preserve_tone=True)
self.image = ImageOps.autocontrast(self.image)
if not self.options.forcecolor:
self.image = self.image.convert('L')
self.crop_main_cover()
self.image.thumbnail(self.options.profileData[1], Image.Resampling.LANCZOS)
self.save()
size = list(self.options.profileData[1])
if self.options.kindle_scribe_azw3:
size[0] = min(size[0], 1920)
size[1] = min(size[1], 1920)
if self.options.coverfill and not self.options.kindle_scribe_azw3:
# TODO: Kindle Scribe case
self.image = ImageOps.fit(self.image, tuple(size), Image.Resampling.LANCZOS, centering=(0.5, 0.5))
else:
self.image.thumbnail(tuple(size), Image.Resampling.LANCZOS)
def crop_main_cover(self):
w, h = self.image.size
if w / h > 2:
if self.options.righttoleft:
self.image = self.image.crop((w/6, 0, w/2 - w * 0.02, h))
else:
self.image = self.image.crop((w/2 + w * 0.02, 0, 5/6 * w, h))
elif w / h > 1.34:
if self.options.righttoleft:
self.image = self.image.crop((0, 0, w/2 - w * 0.03, h))
else:
self.image = self.image.crop((w/2 + w * 0.03, 0, w, h))
def save_to_epub(self, target, tomeid, len_tomes=0):
def save(self):
try:
if tomeid == 0:
self.image.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
else:
copy = self.image.copy()
draw = ImageDraw.Draw(copy)
w, h = copy.size
draw.text(
xy=(w/2, h * .85),
text=f'{tomeid}/{len_tomes}',
anchor='ms',
font_size=h//7,
fill=255,
stroke_fill=0,
stroke_width=25
)
copy.save(target, "JPEG", optimize=1, quality=self.options.jpegquality)
self.image.save(self.target, "JPEG", optimize=1, quality=85)
except IOError:
raise RuntimeError('Failed to save cover.')
def saveToKindle(self, kindle, asin):
self.image = ImageOps.contain(self.image, (300, 470), Image.Resampling.LANCZOS)
self.image = self.image.resize((300, 470), Image.Resampling.LANCZOS)
try:
self.image.save(os.path.join(kindle.path.split('documents')[0], 'system', 'thumbnails',
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=self.options.jpegquality)
'thumbnail_' + asin + '_EBOK_portrait.jpg'), 'JPEG', optimize=1, quality=85)
except IOError:
raise RuntimeError('Failed to upload cover.')

View File

@@ -1,78 +0,0 @@
from PIL import Image, ImageFilter, ImageOps, ImageFile
import numpy as np
from typing import Literal
from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
Crops inter-panel empty spaces (ignores empty spaces near borders - for that use crop margins).
Parameters:
img (PIL image): A PIL image.
direction (horizontal or vertical or both): To crop rows (horizontal), cols (vertical) or both.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
background_color (string): 'white' for white background, anything else for black.
Returns:
img (PIL image): A PIL image after cropping empty sections.
'''
def crop_empty_inter_panel(img, direction: Literal["horizontal", "vertical", "both"], keep=0.04, background_color='white'):
img_temp = img
if img.mode != 'L':
img_temp = ImageOps.grayscale(img_temp)
if background_color != 'white':
img_temp = ImageOps.invert(img_temp)
img_mat = np.array(img)
power = 1
img_temp = ImageOps.autocontrast(img_temp, 1).filter(ImageFilter.BoxBlur(1))
img_temp = img_temp.point(lambda p: 255 if p <= threshold_from_power(power) else 0)
if direction in ["horizontal", "both"]:
rows_idx_to_remove = empty_sections(img_temp, keep, horizontal=True)
img_mat = np.delete(img_mat, rows_idx_to_remove, 0)
if direction in ["vertical", "both"]:
cols_idx_to_remove = empty_sections(img_temp, keep, horizontal=False)
img_mat = np.delete(img_mat, cols_idx_to_remove, 1)
return Image.fromarray(img_mat)
'''
Finds empty sections (excluding near borders).
Parameters:
img (PIL image): A PIL image.
keep (float): Distance to keep between panels after cropping (in percentage relative to the original distance).
horizontal (boolean): True to find empty rows, False to find empty columns.
Returns:
Itertable (list or NumPy array): indices of rows or columns to remove.
'''
def empty_sections(img, keep, horizontal=True):
axis = 1 if horizontal else 0
img_mat = np.array(img)
img_mat_max = np.max(img_mat, axis=axis)
img_mat_empty_idx = np.where(img_mat_max == 0)[0]
empty_sections = group_close_values(img_mat_empty_idx, 1)
sections_to_remove = []
for section in empty_sections:
if section[1] < img.size[1] * 0.99 and section[0] > img.size[1] * 0.01: # if not near borders
sections_to_remove.append(section)
if len(sections_to_remove) != 0:
sections_to_remove_after_keep = [(int(x1+(keep/2)*(x2-x1)), int(x2-(keep/2)*(x2-x1))) for x1,x2 in sections_to_remove]
idx_to_remove = np.concatenate([np.arange(x1, x2) for x1,x2 in sections_to_remove_after_keep])
return idx_to_remove
return []

View File

@@ -19,12 +19,9 @@
import os.path
import psutil
from . import image
class Kindle:
def __init__(self, profile):
self.profile = profile
def __init__(self):
self.path = self.findDevice()
if self.path:
self.coverSupport = self.checkThumbnails()
@@ -32,11 +29,10 @@ class Kindle:
self.coverSupport = False
def findDevice(self):
if self.profile in image.ProfileData.ProfilesKindlePDOC.keys():
return False
for drive in reversed(psutil.disk_partitions(False)):
if (drive[2] == 'FAT32' and drive[3] == 'rw,removable') or \
(drive[2] in ('vfat', 'msdos', 'FAT', 'apfs') and 'rw' in drive[3]):
(drive[2] == 'vfat' and 'rw' in drive[3]) or \
(drive[2] == 'msdos' and 'rw' in drive[3]):
if os.path.isdir(os.path.join(drive[1], 'system')) and \
os.path.isdir(os.path.join(drive[1], 'documents')):
return drive[1]

View File

@@ -20,7 +20,6 @@ import os
from xml.dom.minidom import parse, Document
from tempfile import mkdtemp
from shutil import rmtree
from xml.sax.saxutils import unescape
from . import comicarchive
@@ -53,19 +52,19 @@ class MetadataParser:
def parseXML(self):
if len(self.rawdata.getElementsByTagName('Series')) != 0:
self.data['Series'] = unescape(self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue)
self.data['Series'] = self.rawdata.getElementsByTagName('Series')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Volume')) != 0:
self.data['Volume'] = self.rawdata.getElementsByTagName('Volume')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Number')) != 0:
self.data['Number'] = self.rawdata.getElementsByTagName('Number')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Summary')) != 0:
self.data['Summary'] = unescape(self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue)
self.data['Summary'] = self.rawdata.getElementsByTagName('Summary')[0].firstChild.nodeValue
if len(self.rawdata.getElementsByTagName('Title')) != 0:
self.data['Title'] = unescape(self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue)
self.data['Title'] = self.rawdata.getElementsByTagName('Title')[0].firstChild.nodeValue
for field in ['Writer', 'Penciller', 'Inker', 'Colorist']:
if len(self.rawdata.getElementsByTagName(field)) != 0:
for person in self.rawdata.getElementsByTagName(field)[0].firstChild.nodeValue.split(', '):
self.data[field + 's'].append(unescape(person))
self.data[field + 's'].append(person)
self.data[field + 's'] = list(set(self.data[field + 's']))
self.data[field + 's'].sort()
if len(self.rawdata.getElementsByTagName('Page')) != 0:
@@ -123,4 +122,4 @@ class MetadataParser:
cbx.addFile(tmpXML)
except OSError as e:
raise UserWarning(e)
rmtree(workdir, True)
rmtree(workdir)

View File

@@ -1,206 +0,0 @@
from PIL import ImageOps, ImageFilter, ImageFile
import numpy as np
from .common_crop import threshold_from_power, group_close_values
ImageFile.LOAD_TRUNCATED_IMAGES = True
'''
Some assupmptions on the page number sizes
We assume that the size of the number (including all digits) is between
'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size' relative to the image size.
We assume the distance between the digit is no more than 'max_dist_size' (x,y), and no more than 3 digits.
'''
max_shape_size_tolerated_size = (0.015*3, 0.02) # percent
min_shape_size_tolerated_size = (0.003, 0.006) # percent
window_h_size = max_shape_size_tolerated_size[1]*1.25 # percent
max_dist_size = (0.01, 0.002) # percent
'''
E-reader screen real-estate is an important resource.
More available screensize means more details can be better seen, especially text.
Text is one of the most important elements that need to be clearly readable on e-readers,
which mostly are smaller devices where the need to zoom is unwanted.
By cropping the page number on the bottom of the page, 2%-5% of the page height can be regained
that allows us to upscale the image even more.
- Most of the times the screen height is the limiting factor in upscaling, rather than its width.
Parameters:
img (PIL image): A PIL image.
power (float): The power to 'chop' through pixels matching the background. Values in range[0,3].
background_color (string): 'white' for white background, anything else for black.
Returns:
bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory
'''
def get_bbox_crop_margin_page_number(img, power=1, background_color='white'):
if img.mode != 'L':
img = ImageOps.grayscale(img)
if background_color != 'white':
img = ImageOps.invert(img)
'''
Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites.
Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts.
Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform.
'''
img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1))
'''
The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background)
and the lower the power, more sensitive to black pixels.
'''
threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
bw_bbox = bw_img.getbbox()
if not bw_bbox: # bbox cannot be found in case that the entire resulted image is black.
return None
left, top_y_pos, right, bot_y_pos = bw_bbox
'''
We inspect the lower bottom part of the image where we suspect might be a page number.
We assume that page number consist of 1 to 3 digits and the total min and max size of the number
is between 'min_shape_size_tolerated_size' and 'max_shape_size_tolerated_size'.
'''
window_h = int(img.size[1] * window_h_size)
img_part = img.crop((left,bot_y_pos-window_h, right, bot_y_pos))
'''
We detect related-pixels by proximity, with max distance defined in 'max_dist_size'.
Related pixels (in x axis) for each image-row are then merged to boxes with adjacent rows (in y axis)
to form bounding boxes of the detected objects (which one of them could be the page number).
'''
img_part_mat = np.array(img_part)
window_groups = []
for i in range(img_part.size[1]):
row_groups = [(g[0], g[1], i, i) for g in group_close_values(np.where(img_part_mat[i] <= threshold)[0], img.size[0]*max_dist_size[0])]
window_groups.extend(row_groups)
window_groups = np.array(window_groups)
boxes = merge_boxes(window_groups, (img.size[0]*max_dist_size[0], img.size[1]*max_dist_size[1]))
'''
We assume that the lowest part of the image that has black pixels on is the page number.
In case that there are more than one detected object in the loewst part, we assume that one of them is probably
manga-content and shouldn't be cropped.
'''
# filter all small objects
boxes = list(filter(lambda box: box[1]-box[0] >= img.size[0]*min_shape_size_tolerated_size[0]
and box[3]-box[2] >= img.size[1]*min_shape_size_tolerated_size[1], boxes))
lowest_boxes = list(filter(lambda box: box[3] == window_h-1, boxes))
min_y_of_lowest_boxes = 0
if len(lowest_boxes) > 0:
min_y_of_lowest_boxes = np.min(np.array(lowest_boxes)[:,2])
boxes_in_same_y_range = list(filter(lambda box: box[3] >= min_y_of_lowest_boxes, boxes))
max_shape_size_tolerated = (img.size[0] * max_shape_size_tolerated_size[0],
max(img.size[1] *max_shape_size_tolerated_size[1], 3))
should_force_crop = (
len(boxes_in_same_y_range) == 1
and (boxes_in_same_y_range[0][1] - boxes_in_same_y_range[0][0] <= max_shape_size_tolerated[0])
and (boxes_in_same_y_range[0][3] - boxes_in_same_y_range[0][2] <= max_shape_size_tolerated[1])
)
cropped_bbox = (0, 0, img.size[0], img.size[1])
if should_force_crop:
cropped_bbox = (0, 0, img.size[0], bot_y_pos-(window_h-boxes_in_same_y_range[0][2]+1))
cropped_bbox = bw_img.crop(cropped_bbox).getbbox()
return cropped_bbox
'''
Parameters:
img (PIL image): A PIL image.
power (float): The power to 'chop' through pixels matching the background. Values in range[0,3].
background_color (string): 'white' for white background, anything else for black.
Returns:
bbox (4-tuple, left|top|right|bot): The tightest bounding box calculated after trying to remove the bottom page number. Returns None if couldnt find anything satisfactory
'''
def get_bbox_crop_margin(img, power=1, background_color='white'):
if img.mode != 'L':
img = ImageOps.grayscale(img)
if background_color != 'white':
img = ImageOps.invert(img)
'''
Autocontrast: due to some threshold values, it's important that the blacks will be blacks and white will be whites.
Box/MeanFilter: Allows us to reduce noise like bad a page scan or compression artifacts.
Note: MedianFilter works better in my experience, but takes 2x-3x longer to perform.
'''
img = ImageOps.autocontrast(img, 1).filter(ImageFilter.BoxBlur(1))
'''
The 'power' parameters determines the threshold. The higher the power, the more "force" it can crop through black pixels (in case of white background)
and the lower the power, more sensitive to black pixels.
'''
threshold = threshold_from_power(power)
bw_img = img.point(lambda p: 255 if p <= threshold else 0)
ignore_pixels_near_edge(bw_img)
return bw_img.getbbox()
def ignore_pixels_near_edge(bw_img):
w, h = bw_img.size
edge_bbox = [
(0, 0, w, int(0.02 * h)),
(0, int(0.98 * h), w, h),
(0, 0, int(0.02 * w), h),
(int(0.98 * w), 0, w, h)
]
for box in edge_bbox:
edge = bw_img.crop(box)
h = edge.histogram()
if not edge.height or not edge.width:
continue
imperfections = h[255] / (edge.height * edge.width)
if imperfections > 0 and imperfections < .02:
bw_img.paste(im=0, box=box)
def box_intersect(box1, box2, max_dist):
return not (box2[0]-max_dist[0] > box1[1]
or box2[1]+max_dist[0] < box1[0]
or box2[2]-max_dist[1] > box1[3]
or box2[3]+max_dist[1] < box1[2])
'''
Merge close bounding boxes (left,right, top,bot) (x axis) with distance threshold defined in
'max_dist_tolerated'. Boxes with less 'max_dist_tolerated' distance (Chebyshev distance).
'''
def merge_boxes(boxes, max_dist_tolerated):
j = 0
while j < len(boxes)-1:
g1 = boxes[j]
intersecting_boxes = []
other_boxes = []
for i in range(j+1,len(boxes)):
g2 = boxes[i]
if box_intersect(g1,g2, max_dist_tolerated):
intersecting_boxes.append(g2)
else:
other_boxes.append(g2)
if len(intersecting_boxes) > 0:
intersecting_boxes = np.array([g1, *intersecting_boxes])
merged_box = np.array([
np.min(intersecting_boxes[:,0]),
np.max(intersecting_boxes[:,1]),
np.min(intersecting_boxes[:,2]),
np.max(intersecting_boxes[:,3])
])
other_boxes.append(merged_box)
boxes = np.concatenate([boxes[:j], other_boxes])
j = 0
else:
j += 1
return boxes

View File

@@ -22,6 +22,8 @@
#
import os
from random import choice
from string import ascii_uppercase, digits
# skip stray images a few pixels in size in some PDFs
# typical images are many thousands in length
@@ -30,9 +32,10 @@ STRAY_IMAGE_LENGTH_THRESHOLD = 300
class PdfJpgExtract:
def __init__(self, fname, fullPath):
def __init__(self, fname):
self.fname = fname
self.path = fullPath
self.filename = os.path.splitext(fname)
self.path = self.filename[0] + "-KCC-" + ''.join(choice(ascii_uppercase + digits) for _ in range(3))
def getPath(self):
return self.path
@@ -45,6 +48,7 @@ class PdfJpgExtract:
endfix = 2
i = 0
njpg = 0
os.makedirs(self.path)
while True:
istream = pdf.find(b"stream", i)
if istream < 0:
@@ -67,9 +71,9 @@ class PdfJpgExtract:
continue
jpg = pdf[istart:iend]
jpgfile = open(os.path.join(self.path, "jpg%d.jpg" % njpg), "wb")
jpgfile = open(self.path + "/jpg%d.jpg" % njpg, "wb")
jpgfile.write(jpg)
jpgfile.close()
njpg += 1
return njpg
return self.path, njpg

View File

@@ -1,249 +0,0 @@
import numpy as np
from PIL import Image, ImageFile
ImageFile.LOAD_TRUNCATED_IMAGES = True
def fourier_transform_image(img):
"""
Memory-optimized version that modifies the array in place when possible.
"""
# Convert with minimal copy
img_array = np.asarray(img, dtype=np.float32)
# Use rfft2 if the image is real to save memory
# and computation time (approximately 2x faster)
fft_result = np.fft.rfft2(img_array)
return fft_result
def attenuate_diagonal_frequencies(fft_spectrum, freq_threshold=0.30, target_angle=135,
angle_tolerance=10, attenuation_factor=0.10):
"""
Attenuates specific frequencies in the Fourier domain (optimized version for rfft2).
Args:
fft_spectrum: Result of 2D real Fourier transform (from rfft2)
freq_threshold: Frequency threshold in cycles/pixel (default: 0.3, theoretical max: 0.5)
target_angle: Target angle in degrees (default: 135)
angle_tolerance: Angular tolerance in degrees (default: 15)
attenuation_factor: Attenuation factor (0.1 = 90% attenuation)
Returns:
np.ndarray: Modified FFT with applied attenuation (same format as input)
"""
# Get dimensions of the rfft2 result
if fft_spectrum.ndim == 2:
height, width_rfft = fft_spectrum.shape
else: # 3D array (color channels)
height, width_rfft = fft_spectrum.shape[:2]
# For rfft2, the original width is (width_rfft - 1) * 2
width_original = (width_rfft - 1) * 2
# Create frequency grids for rfft2 format
freq_y = np.fft.fftfreq(height, d=1.0)
freq_x = np.fft.rfftfreq(width_original, d=1.0) # Use rfftfreq for the X dimension
# Use broadcasting to create grids without meshgrid (more efficient)
freq_y_grid = freq_y.reshape(-1, 1) # Column
freq_x_grid = freq_x.reshape(1, -1) # Row
# Calculate squared radial frequencies (avoid sqrt)
freq_radial_sq = freq_x_grid**2 + freq_y_grid**2
freq_threshold_sq = freq_threshold**2
# Frequency condition
freq_condition = freq_radial_sq >= freq_threshold_sq
# Early exit if no frequency satisfies the condition
if not np.any(freq_condition):
return fft_spectrum
# Calculate angles only where necessary
# Use atan2 directly with broadcasting
angles_rad = np.arctan2(freq_y_grid, freq_x_grid)
# Convert to degrees and normalize in a single operation
angles_deg = np.rad2deg(angles_rad) % 360
# Calculation of complementary angle
target_angle_2 = (target_angle + 180) % 360
# Calulation of perpendicular angles (135° + 45° to maximize compatibility until we know for sure which angle configure for each device)
target_angle_3 = (target_angle + 90) % 360
target_angle_4 = (target_angle_3 + 180) % 360
# Create angular conditions in a vectorized way
angle_condition = np.zeros_like(angles_deg, dtype=bool)
# Process both angles simultaneously
for angle in [target_angle, target_angle_2, target_angle_3, target_angle_4]:
min_angle = (angle - angle_tolerance) % 360
max_angle = (angle + angle_tolerance) % 360
if min_angle > max_angle: # Interval crosses 0°
angle_condition |= (angles_deg >= min_angle) | (angles_deg <= max_angle)
else: # Normal interval
angle_condition |= (angles_deg >= min_angle) & (angles_deg <= max_angle)
# Combine conditions
combined_condition = freq_condition & angle_condition
# Apply attenuation directly (avoid creating a full mask)
if attenuation_factor == 0:
# Special case: complete suppression
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] = 0
else: # 3D array
fft_spectrum[combined_condition, :] = 0
return fft_spectrum
elif attenuation_factor == 1:
# Special case: no attenuation
return fft_spectrum
else:
# General case: partial attenuation
if fft_spectrum.ndim == 2:
fft_spectrum[combined_condition] *= attenuation_factor
else: # 3D array
fft_spectrum[combined_condition, :] *= attenuation_factor
return fft_spectrum
def inverse_fourier_transform_image(fft_spectrum, is_color, original_shape=None):
"""
Performs an optimized inverse Fourier transform to reconstruct a PIL image.
Args:
fft_spectrum: Fourier transform result (complex array from rfft2)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Reconstructed image
"""
# Perform inverse Fourier transform with original shape if provided
if original_shape is not None:
img_reconstructed = np.fft.irfft2(fft_spectrum, s=original_shape)
else:
img_reconstructed = np.fft.irfft2(fft_spectrum)
# Normalize values between 0 and 255
img_reconstructed = np.clip(img_reconstructed, 0, 255)
img_reconstructed = img_reconstructed.astype(np.uint8)
# Convert to PIL image
if is_color and img_reconstructed.ndim == 3:
pil_image = Image.fromarray(img_reconstructed, mode='RGB')
else:
pil_image = Image.fromarray(img_reconstructed, mode='L')
return pil_image
def rgb_to_yuv(rgb_array):
"""
Convert RGB to YUV color space.
Y = luminance, U and V = chrominance
"""
# Coefficients for RGB to YUV conversion
rgb_to_yuv_matrix = np.array([
[0.299, 0.587, 0.114], # Y
[-0.14713, -0.28886, 0.436], # U
[0.615, -0.51499, -0.10001] # V
])
# Reshape for matrix multiplication
original_shape = rgb_array.shape
rgb_flat = rgb_array.reshape(-1, 3)
# Apply transformation
yuv_flat = rgb_flat @ rgb_to_yuv_matrix.T
# Reshape back
yuv_array = yuv_flat.reshape(original_shape)
return yuv_array
def yuv_to_rgb(yuv_array):
"""
Convert YUV to RGB color space.
"""
# Coefficients for YUV to RGB conversion
yuv_to_rgb_matrix = np.array([
[1.0, 0.0, 1.13983], # R
[1.0, -0.39465, -0.58060], # G
[1.0, 2.03211, 0.0] # B
])
# Reshape for matrix multiplication
original_shape = yuv_array.shape
yuv_flat = yuv_array.reshape(-1, 3)
# Apply transformation
rgb_flat = yuv_flat @ yuv_to_rgb_matrix.T
# Reshape back
rgb_array = rgb_flat.reshape(original_shape)
return rgb_array
def erase_rainbow_artifacts(img, is_color):
"""
Remove rainbow artifacts from grayscale or color images.
Args:
img: PIL Image (grayscale or RGB)
is_color: Boolean indicating if the image is to be treated as color
Returns:
PIL.Image: Cleaned image
"""
# Auto-detect color mode if not specified
if is_color is None:
color = img.mode in ('RGB', 'RGBA', 'L') and len(np.array(img).shape) == 3
if is_color and img.mode in ('RGB', 'RGBA'):
# Convert to RGB if needed
if img.mode == 'RGBA':
img = img.convert('RGB')
# Convert to numpy array
img_array = np.array(img, dtype=np.float32)
# Convert to YUV color space
yuv_array = rgb_to_yuv(img_array)
# Extract luminance channel (Y)
luminance = yuv_array[:, :, 0]
# Process only the luminance channel
fft_spectrum = fourier_transform_image(luminance)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_luminance = np.fft.irfft2(clean_spectrum, s=luminance.shape)
# Normalize and clip luminance
clean_luminance = np.clip(clean_luminance, 0, 255)
# Replace luminance in YUV array
yuv_array[:, :, 0] = clean_luminance
# Convert back to RGB
rgb_array = yuv_to_rgb(yuv_array)
rgb_array = np.clip(rgb_array, 0, 255).astype(np.uint8)
# Convert back to PIL image
clean_image = Image.fromarray(rgb_array, mode='RGB')
else:
# Grayscale processing (original behavior)
if img.mode != 'L':
img = img.convert('L')
# Get original image dimensions
original_shape = (img.height, img.width)
fft_spectrum = fourier_transform_image(img)
clean_spectrum = attenuate_diagonal_frequencies(fft_spectrum)
clean_image = inverse_fourier_transform_image(clean_spectrum, is_color, original_shape)
return clean_image

View File

@@ -19,17 +19,13 @@
#
import os
from hashlib import md5
from html.parser import HTMLParser
import subprocess
from packaging.version import Version
from distutils.version import StrictVersion
from re import split
import sys
from traceback import format_tb
IMAGE_TYPES = ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.jp2', '.avif')
class HTMLStripper(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
@@ -48,17 +44,11 @@ class HTMLStripper(HTMLParser):
pass
def dot_clean(filetree):
for root, _, files in os.walk(filetree, topdown=False):
for name in files:
if name.startswith('._') or name == '.DS_Store':
if os.path.exists(os.path.join(root, name)):
os.remove(os.path.join(root, name))
def getImageFileName(imgfile):
name, ext = os.path.splitext(imgfile)
ext = ext.lower()
if (name.startswith('.') and len(name) == 1) or ext not in ['.png', '.jpg', '.jpeg', '.gif', '.webp']:
return None
return [name, ext]
@@ -82,6 +72,16 @@ def walkLevel(some_dir, level=1):
del dirs[:]
def md5Checksum(fpath):
with open(fpath, 'rb') as fh:
m = md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
return m.hexdigest()
def sanitizeTrace(traceback):
return ''.join(format_tb(traceback))\
@@ -101,10 +101,10 @@ def dependencyCheck(level):
if level > 2:
try:
from PySide6.QtCore import qVersion as qtVersion
if Version('6.0.0') > Version(qtVersion()):
missing.append('PySide 6.0.0')
if StrictVersion('6.5.1') > StrictVersion(qtVersion()):
missing.append('PySide 6.5.1+')
except ImportError:
missing.append('PySide 6.0.0+')
missing.append('PySide 6.5.1+')
try:
import raven
except ImportError:
@@ -112,7 +112,7 @@ def dependencyCheck(level):
if level > 1:
try:
from psutil import __version__ as psutilVersion
if Version('5.0.0') > Version(psutilVersion):
if StrictVersion('5.0.0') > StrictVersion(psutilVersion):
missing.append('psutil 5.0.0+')
except ImportError:
missing.append('psutil 5.0.0+')
@@ -121,27 +121,16 @@ def dependencyCheck(level):
from slugify import __version__ as slugifyVersion
if isinstance(slugifyVersion, ModuleType):
slugifyVersion = slugifyVersion.__version__
if Version('1.2.1') > Version(slugifyVersion):
if StrictVersion('1.2.1') > StrictVersion(slugifyVersion):
missing.append('python-slugify 1.2.1+')
except ImportError:
missing.append('python-slugify 1.2.1+')
try:
from PIL import __version__ as pillowVersion
if Version('8.3.0') > Version(pillowVersion):
missing.append('Pillow 8.3.0+')
if StrictVersion('5.2.0') > StrictVersion(pillowVersion):
missing.append('Pillow 5.2.0+')
except ImportError:
missing.append('Pillow 8.3.0+')
try:
from pymupdf import __version__ as pymupdfVersion
if Version('1.16.1') > Version(pymupdfVersion):
missing.append('PyMuPDF 1.16.1+')
except ImportError:
missing.append('PyMuPDF 1.16.1+')
missing.append('Pillow 5.2.0+')
if len(missing) > 0:
print('ERROR: ' + ', '.join(missing) + ' is not installed!')
sys.exit(1)
def subprocess_run(command, **kwargs):
if (os.name == 'nt'):
kwargs.setdefault('creationflags', subprocess.CREATE_NO_WINDOW)
return subprocess.run(command, **kwargs)
exit(1)

BIN
other/windows/7z.dll Normal file

Binary file not shown.

BIN
other/windows/7z.exe Normal file

Binary file not shown.

View File

@@ -0,0 +1,90 @@
7-Zip
~~~~~
License for use and distribution
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
7-Zip Copyright (C) 1999-2018 Igor Pavlov.
The licenses for files are:
1) 7z.dll:
- The "GNU LGPL" as main license for most of the code
- The "GNU LGPL" with "unRAR license restriction" for some code
- The "BSD 3-clause License" for some code
2) All other files: the "GNU LGPL".
Redistributions in binary form must reproduce related license information from this file.
Note:
You can use 7-Zip on any computer, including a computer in a commercial
organization. You don't need to register or pay for 7-Zip.
GNU LGPL information
--------------------
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You can receive a copy of the GNU Lesser General Public License from
http://www.gnu.org/
BSD 3-clause License
--------------------
The "BSD 3-clause License" is used for the code in 7z.dll that implements LZFSE data decompression.
That code was derived from the code in the "LZFSE compression library" developed by Apple Inc,
that also uses the "BSD 3-clause License":
----
Copyright (c) 2015-2016, Apple Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder(s) nor the names of any contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
----
unRAR license restriction
-------------------------
The decompression engine for RAR archives was developed using source
code of unRAR program.
All copyrights to original unRAR code are owned by Alexander Roshal.
The license for original unRAR code has the following restriction:
The unRAR sources cannot be used to re-create the RAR compression algorithm,
which is proprietary. Distribution of modified unRAR sources in separate form
or as a part of other software is permitted, provided that it is clearly
stated in the documentation and source comments that the code may
not be used to develop a RAR (WinRAR) compatible archiver.
--
Igor Pavlov

View File

@@ -0,0 +1,233 @@
{\rtf1\adeflang1025\ansi\ansicpg1250\uc1\adeff0\deff0\stshfdbch0\stshfloch31506\stshfhich31506\stshfbi31506\deflang1045\deflangfe1045\themelang1045\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\f0\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f37\fbidi \fswiss\fcharset238\fprq2{\*\panose 020f0502020204030204}Calibri;}
{\flomajor\f31500\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbmajor\f31501\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fhimajor\f31502\fbidi \fswiss\fcharset238\fprq2{\*\panose 020f0302020204030204}Calibri Light;}{\fbimajor\f31503\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\flominor\f31504\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbminor\f31505\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fhiminor\f31506\fbidi \fswiss\fcharset238\fprq2{\*\panose 020f0502020204030204}Calibri;}{\fbiminor\f31507\fbidi \froman\fcharset238\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f44\fbidi \froman\fcharset0\fprq2 Times New Roman;}
{\f43\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f45\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f46\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f47\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\f48\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f49\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f50\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f44\fbidi \froman\fcharset0\fprq2 Times New Roman;}
{\f43\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f45\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f46\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f47\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\f48\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f49\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f50\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f414\fbidi \fswiss\fcharset0\fprq2 Calibri;}
{\f413\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\f415\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f416\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f417\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}
{\f418\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\f419\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f420\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\flomajor\f31510\fbidi \froman\fcharset0\fprq2 Times New Roman;}
{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31520\fbidi \froman\fcharset0\fprq2 Times New Roman;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
{\fhimajor\f31530\fbidi \fswiss\fcharset0\fprq2 Calibri Light;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}
{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}
{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31540\fbidi \froman\fcharset0\fprq2 Times New Roman;}
{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31550\fbidi \froman\fcharset0\fprq2 Times New Roman;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
{\fdbminor\f31560\fbidi \froman\fcharset0\fprq2 Times New Roman;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31570\fbidi \fswiss\fcharset0\fprq2 Calibri;}
{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}
{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}
{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31580\fbidi \froman\fcharset0\fprq2 Times New Roman;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}
{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;
\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\chyperlink\ctint255\cshade255\red5\green99\blue193;\cfollowedhyperlink\ctint255\cshade255\red149\green79\blue114;}{\*\defchp
\f31506\fs22\lang1045\langfe1033\langfenp1033 }{\*\defpap \ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{\ql \li0\ri0\sa160\sl259\slmult1
\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang1045\langfe1033\cgrid\langnp1045\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{\*\cs10 \additive
\ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\*
\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1
\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31506\afs22\alang1025 \ltrch\fcs0 \f31506\fs22\lang1045\langfe1033\cgrid\langnp1045\langfenp1033 \snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive
\rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf17 \sbasedon10 \sunhideused \styrsid3562894 Hyperlink;}{\*\cs16 \additive \rtlch\fcs1 \af0 \ltrch\fcs0 \ul\cf18 \sbasedon10 \ssemihidden \sunhideused \styrsid7678248 FollowedHyperlink;}}{\*\rsidtbl \rsid1081196
\rsid3146412\rsid3562894\rsid5731975\rsid7678248\rsid9265883\rsid11107340\rsid11629590\rsid12600926\rsid13187577}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info
{\author Pawe\'b3 Jastrz\'eabski}{\operator Pawe\'b3 Jastrz\'eabski}{\creatim\yr2013\mo10\dy29\hr15\min17}{\revtim\yr2017\mo8\dy20\hr17\min40}{\version9}{\edmins8}{\nofpages1}{\nofwords33}{\nofchars201}{\nofcharsws233}{\vern39}}{\*\xmlnstbl {\xmlns1 http:
//schemas.microsoft.com/office/word/2003/wordml}}\paperw11906\paperh16838\margl1417\margr1417\margt1417\margb1417\gutter0\ltrsect
\deftab708\widowctrl\ftnbj\aenddoc\hyphhotz425\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0
\showxmlerrors1\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin1417\dgvorigin1417\dghshow1\dgvshow1
\jexpand\viewkind1\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct
\asianbrkrule\rsidroot11107340\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0
{\*\wgrffmtfilter 2450}\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\headery708\footery708\colsx708\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2
\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6
\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang
{\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0
\f31506\fs22\lang1045\langfe1033\cgrid\langnp1045\langfenp1033 {\rtlch\fcs1 \af0 \ltrch\fcs0 \b\fs52\cf6\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid3562894 Warning!}{\rtlch\fcs1 \af0 \ltrch\fcs0
\b\fs52\cf6\lang2057\langfe1033\langnp2057\insrsid13187577\charrsid3562894
\par }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid1081196 Creation of}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 MOBI}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 files }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\fs28\lang2057\langfe1033\langnp2057\insrsid5731975\charrsid12600926 require}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid11629590 s}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\fs28\lang2057\langfe1033\langnp2057\insrsid5731975\charrsid12600926 additional software.}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926
\par }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 Please download: }{\field\flddirty{\*\fldinst {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 HYPERLINK "http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000765211" }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926
{\*\datafield
00d0c9ea79f9bace118c8200aa004ba90b0200000003000000e0c9ea79f9bace118c8200aa004ba90b9600000068007400740070003a002f002f007700770077002e0061006d0061007a006f006e002e0063006f006d002f00670070002f0066006500610074007500720065002e00680074006d006c003f00690065003d00
5500540046003800260064006f006300490064003d0031003000300030003700360035003200310031000000795881f43b1d7f48af2c825dc485276300000000a5ab00000000}}}{\fldrslt {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\cs15\b\fs28\ul\cf17\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 KindleGen}}}\sectd \ltrsect\linex0\headery708\footery708\colsx708\endnhere\sectlinegrid360\sectdefaultcl\sftnbj {\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926
\par }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 And place }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \i\fs28\lang2057\langfe1033\langnp2057\insrsid5731975\charrsid12600926 kindlegen.exe}{\rtlch\fcs1
\af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid5731975\charrsid12600926 }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 inside }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412\charrsid3146412 Kindle}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412 }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412\charrsid3146412 Comic}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412 }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412\charrsid3146412 Converter}{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0 \b\fs28\lang2057\langfe1033\langnp2057\insrsid3146412 }{\rtlch\fcs1 \af0\afs24 \ltrch\fcs0
\fs28\lang2057\langfe1033\langnp2057\insrsid3562894\charrsid12600926 directory.
\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a
9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad
5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6
b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0
0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6
a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f
c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512
0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462
a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865
6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b
4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b
4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b7e72e45da060000a81a0000160000007468656d652f7468656d652f
7468656d65312e786d6cec595d8b1b37147d2ff43f0cf3eef86bc61f4bbcc11edbd936bbc9123b2979d4dab24759cdc88ce4dd981028c963a1509a963e34d0b7
3e94b68104fa92fe9a6d53da14f2177aa5198f255bdb4d96149692352c63f9dcaba37bef9cab195dbe722fa2ce114e386171cb2d5f2ab90e8e476c4ce269cbbd
35ec171aaec3058ac788b218b7dc05e6ee95ed0f3fb88cb6448823ec807dccb750cb0d85986d158b7c04c3885f62331cc36f13964448c0d7645a1c27e818fc46
b45829956ac50891d875621481db3d2616c7ce0c1d122ce6ce8dc9848cb0bbbd9ca44761a658703930a2c9404e8133cb7d65a39b8c0fcb12c8173ca089738468
cb8569c7ec7888ef09d7a1880bf8a1e596d49f5bdcbe5c445b991115a7d86a767df597d96506e3c38a9a33991ee4937a9eefd5dab97f05a06213d7abf76abd5a
ee4f01d068040b4eb9e83efd4eb3d3f533ac064a2f2dbebbf56eb56ce035ffd50dce6d5f7e0cbc02a5febd0d7cbf1f40140dbc02a5787f03ef79f54ae0197805
4af1b50d7cbdd4ee7a7503af402125f1e106bae4d7aac172b53964c2e88e15def4bd7ebd92395fa1a01af22293534c582cce28b908dd65491f70124f9120b123
16333c4123a8ed005172901067974c43a8bf198a1987e152a5d42f55e1bffc78ea4a05066d61a4594b7a40886f0c495a0e1f2564265aeec7e0d5d520af5ffcf8
fac533e7e4e1f39387bf9c3c7a74f2f0e7d49161b583e2a96ef5eafb2ffe7ef2a9f3d7b3ef5e3dfeca8ee73afef79f3efbedd72fed4058e92a042fbf7efac7f3
a72fbff9fccf1f1e5be0ed041de8f021893077aee363e7268b60612a0426737c90bc9dc5304444b768c7538e622467b1f8ef89d0405f5f208a2cb80e3623783b
01a5b101afceef1a8407613217c4e2f15a1819c03dc6688725d6285c937369611ecee3a97df264aee36e2274649b3b40b191dfde7c064a4b6c2e83101b34f729
8a059ae2180b47fec60e31b6acee0e21465cf7c828619c4d847387381d44ac21199203a39a56463b2482bc2c6c0421df466cf66e3b1d466dabeee22313097705
a216f2434c8d305e457381229bcb218aa81ef05d24421bc9c12219e9b81e1790e929a6cce98d31e7369b1b09ac574bfa3590177bdaf7e8223291892087369fbb
88311dd965874188a2990d3b2071a8633fe28750a2c8d967c206df63e61d22bf431e507c6aba6fc31e409fe06c35b805caaa5bac0a44fe324f2cb9bc8a9951bf
83059d20aca406f4dfd0f388c4678afb9aacfbffadac8390befcf68965551755d0db09b1de513b6b327e1a6e5dbc03968cc9c5d7ee2e9ac7fb186e97cd06f65e
badf4bb7fbbf97eed3eee7772fd82b8d06f9965bc574c7aef6efd159dbf709a174201614ef72b583e7d0a0c67d1894e6ea8116e74f75b3102ee50d0df318b869
82948d9330f10911e1204433d8e6975de964ca33d753eecc1887ddbf1ab6fa96783a8ff6d8387d782d97e5836aaa211c89d578c9cfc7e18943a4e85a7df54096
bb576ca7eaf9794940dabe0d096d329344d542a2be1c9441524feb10340b09b5b277c2a26961d190ee97a9da6001d4f2acc00eca817d57cbf53d30012378b042
148f659ed2542fb3ab92f92e337d5a308d0a80edc4b20256996e4aaea72e4fae2e2db537c8b441422b3793848a8c6a653c44639c55a71c7d131a6f9bebe62aa5
063d190a351f94d68a46bdf16f2cce9b6bb05bd7061aeb4a4163e7b8e5d6aa3e94cc08cd5aee049efee1329a41ed70b9f345740a2fd64622496ff8f328cb2ce1
a28b7898065c894eaa061111387128895aae5c7e9e061a2b0d51dcca1510840b4bae09b272d1c841d2cd24e3c9048f849e766d44463afd0a0a9f6a85f557657e
7eb0b4647348f7201c1f3b07749edc4450627ebd2c0338261c5e0295d3688e09bcdccc856c557f6b8d29935dfdeda2aaa1741cd15988b28ea28b790a57529ed3
51dff21868dfb2354340b590648df0602a1bac1e54a39be65d23e5706ad73ddb48464e13cd55cf345445764dbb8a19332cdbc05a2ccfd7e43556cb1083a6e91d
3e95ee75c96d2eb56e6d9f90770908781e3f4bd77d8386a0515b4d6650938c3765586a76366af68ee502cfa0f6264d4253fddad2ed5adcf21e619d0e06cfd5f9
c16ebd6a6168b2dc5eaa48ab4311fdbc821ddc05f1e8c2bbe039155ca5128e2112041ba281da93a4b201b7c83d91dd1a70e5cc13d272ef97fcb61754fca0506a
f8bd8257f54a8586dfae16dabe5f2df7fc72a9dba93c80c622c2a8eca707327d78234517d9b18c1adf389a89962fdd2e8d585464eab0a5a888aba39972c5389a
490f639ca13c73711d02a273bf56e937abcd4eadd0acb6fb05afdb69149a41ad53e8d6827ab7df0dfc46b3ffc0758e14d86b5703afd66b146ae5202878b592a4
df6816ea5ea5d2f6eaed46cf6b3fc8b631b0f2543eb258407815afed7f000000ffff0300504b0304140006000800000021000dd1909fb60000001b0100002700
00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0ad
d40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b
284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f16
5dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000
000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b000000
00000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000
000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b7e72e45da060000a81a000016
00000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b
0100002700000000000000000000000000e40900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000df0a00000000}
{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d
617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169
6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363
656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}
{\*\latentstyles\lsdstimax375\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;
\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;
\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;
\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;
\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;
\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;
\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;
\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;
\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;
\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;
\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;
\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;
\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;
\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;
\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;
\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;
\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;
\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;
\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;
\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;
\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;
\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;}}{\*\datastore 010500000200000018000000
4d73786d6c322e534158584d4c5265616465722e362e30000000000000000000000e0000
d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff0900060000000000000000000000010000000100000000000000001000000200000001000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
fffffffffffffffffdffffff04000000feffffff05000000fefffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffff010000000c6ad98892f1d411a65f0040963251e5000000000000000000000000b00f
9da8ca19d30103000000c0020000000000004d0073006f004400610074006100530074006f0072006500000000000000000000000000000000000000000000000000000000000000000000000000000000001a000101ffffffffffffffff020000000000000000000000000000000000000000000000b00f9da8ca19d301
b00f9da8ca19d301000000000000000000000000ca0041004300c300d300d300c70058004d00d4003000c9004d00c200590043003100320055004a00300051003d003d000000000000000000000000000000000032000101ffffffffffffffff030000000000000000000000000000000000000000000000b00f9da8ca19
d301b00f9da8ca19d3010000000000000000000000004900740065006d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a000201ffffffff04000000ffffffff000000000000000000000000000000000000000000000000
00000000000000000000000000000000320100000000000001000000020000000300000004000000feffffff060000000700000008000000090000000a000000feffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d226e6f223f3e3c623a536f75726365732053656c65637465645374796c653d225c41504153697874684564697469
6f6e4f66666963654f6e6c696e652e78736c22205374796c654e616d653d22415041222056657273696f6e3d22362220786d6c6e733a623d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f6f6666696365446f63756d656e742f323030362f6269626c696f677261706879222078
6d6c6e733d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f6f6666696365446f63756d656e742f323030362f6269626c696f677261706879223e3c2f623a536f75726365733e00000000000000000000000000003c3f786d6c2076657273696f6e3d22312e302220656e636f6469
6e673d225554462d3822207374616e64616c6f6e653d226e6f223f3e0d0a3c64733a6461746173746f72654974656d2064733a6974656d49443d227b43464133303041382d443733392d343633332d413933322d3236303236444335303936397d2220786d6c6e733a64733d22687474703a2f2f736368656d61732e6f70
656e786d6c666f726d6174732e6f72672f6f6666696365446f63756d656e742f323030362f637573500072006f007000650072007400690065007300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000200ffffffffffffffffffffffff000000000000
0000000000000000000000000000000000000000000000000000000000000500000055010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000746f6d586d6c223e3c64733a736368656d61526566733e3c64733a736368656d615265662064733a7572693d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f7267
2f6f6666696365446f63756d656e742f323030362f6269626c696f677261706879222f3e3c2f64733a736368656d61526566733e3c2f64733a6461746173746f72654974656d3e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105000000000000}}

View File

@@ -1,11 +0,0 @@
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
# Below requirements are compiled in Dockefile
# numpy==2.3.4
# PyMuPDF==1.26.6

View File

@@ -1,12 +0,0 @@
PySide6==6.4.3
Pillow>=11.3.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy<2
PyMuPDF>=1.26.1

View File

@@ -1,12 +0,0 @@
PySide6==6.1.3
Pillow>=9
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy==1.23.0
PyMuPDF>=1.16

View File

@@ -1,12 +1,7 @@
PySide6>6
Pillow>=11.3.0
PySide6>=6.5.1
Pillow>=5.2.0
psutil>=5.9.5
requests>=2.31.0
python-slugify>=1.2.1,<9.0.0
python-slugify>=1.2.1
raven>=6.0.0
packaging>=23.2
mozjpeg-lossless-optimization>=1.2.0
natsort>=8.4.0
distro>=1.8.0
numpy>=1.22.4
PyMuPDF>=1.18.0
mozjpeg-lossless-optimization>=1.1.2
distro

106
setup.py
View File

@@ -8,14 +8,13 @@ Install as Python package:
Create EXE/APP:
python3 setup.py build_binary
python3 setup.py build_c2e
python3 setup.py build_c2p
"""
import os
import platform
import sys
import shutil
import setuptools
import distutils.cmd
from kindlecomicconverter import __version__
NAME = 'KindleComicConverter'
@@ -24,7 +23,7 @@ VERSION = __version__
# noinspection PyUnresolvedReferences
class BuildBinaryCommand(setuptools.Command):
class BuildBinaryCommand(distutils.cmd.Command):
description = 'build binary release'
user_options = []
@@ -38,96 +37,24 @@ class BuildBinaryCommand(setuptools.Command):
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
os.system('pyinstaller -y -F -i icons/comic2ebook.icns -n "Kindle Comic Converter" -w -s kcc.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
min_os = os.getenv('MACOSX_DEPLOYMENT_TARGET', '')
if min_os.startswith('10.1'):
os.system(f'appdmg kcc.json dist/kcc_osx_{min_os.replace(".", "_")}_legacy_{VERSION}.dmg')
else:
os.system(f'appdmg kcc.json dist/kcc_macos_{platform.processor()}_{VERSION}.dmg')
sys.exit(0)
os.system('appdmg kcc.json dist/KindleComicConverter_osx_' + VERSION + '.dmg')
exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_win7_legacy_' + VERSION + ' -w --noupx kcc.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
sys.exit(0)
os.system('pyinstaller -y -F -i icons\\comic2ebook.ico -n KCC_' + VERSION + ' -w --noupx kcc.py')
exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py')
sys.exit(0)
'pyinstaller --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_linux_' + VERSION + ' kcc.py')
exit(0)
else:
sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2ECommand(setuptools.Command):
description = 'build binary c2e release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -D -i icons/comic2ebook.icns -n "KCC C2E" -c -s kcc-c2e.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_win7_legacy_' + VERSION + ' -c --noupx kcc-c2e.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2e_' + VERSION + ' -c --noupx kcc-c2e.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2e_linux_' + VERSION + ' kcc-c2e.py')
sys.exit(0)
else:
sys.exit(0)
# noinspection PyUnresolvedReferences
class BuildC2PCommand(setuptools.Command):
description = 'build binary c2p release'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
# noinspection PyShadowingNames
def run(self):
VERSION = __version__
if sys.platform == 'darwin':
os.system('pyinstaller --hidden-import=_cffi_backend -y -n "KCC C2P" -c -s kcc-c2p.py')
# TODO /usr/bin/codesign --force -s "$MACOS_CERTIFICATE_NAME" --options runtime dist/Applications/Kindle\ Comic\ Converter.app -v
sys.exit(0)
elif sys.platform == 'win32':
if os.getenv('WINDOWS_7'):
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_win7_legacy_' + VERSION + ' -c --noupx kcc-c2p.py')
else:
os.system('pyinstaller --hidden-import=_cffi_backend -y -F -i icons\\comic2ebook.ico -n kcc_c2p_' + VERSION + ' -c --noupx kcc-c2p.py')
sys.exit(0)
elif sys.platform == 'linux':
os.system(
'pyinstaller --hidden-import=_cffi_backend --hidden-import=queue -y -F -i icons/comic2ebook.ico -n kcc_c2p_linux_' + VERSION + ' kcc-c2p.py')
sys.exit(0)
else:
sys.exit(0)
exit(0)
setuptools.setup(
cmdclass={
'build_binary': BuildBinaryCommand,
'build_c2e': BuildC2ECommand,
'build_c2p': BuildC2PCommand,
},
name=NAME,
version=VERSION,
@@ -148,17 +75,12 @@ setuptools.setup(
},
packages=['kindlecomicconverter'],
install_requires=[
'PySide6>=6.0.0',
'Pillow>=9.3.0',
'pyside6>=6.5.1',
'Pillow>=5.2.0',
'psutil>=5.9.5',
'requests>=2.31.0',
'python-slugify>=1.2.1,<9.0.0',
'raven>=6.0.0',
'mozjpeg-lossless-optimization>=1.2.0',
'natsort>=8.4.0',
'distro>=1.8.0',
'numpy>=1.22.4',
'PyMuPDF>=1.16.1',
'mozjpeg-lossless-optimization>=1.1.2',
],
classifiers=[],
zip_safe=False,