Skip to content

Commit

Permalink
Merge branch 'unstable' into stable
Browse files Browse the repository at this point in the history
  • Loading branch information
JacobDev1 committed Jun 18, 2024
2 parents a9b563f + 1682e01 commit ad7c67b
Show file tree
Hide file tree
Showing 39 changed files with 1,951 additions and 631 deletions.
66 changes: 28 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<img src="icons/logo.svg" width="20%">
<h3 align="center">XL Converter</h3>

Powerful image converter for the latest formats with support for multithreading, drag 'n drop, and downscaling.
Easy-to-use image converter for modern formats. Supports multithreading, drag 'n drop, and downscaling.

Available for Windows and Linux.

Expand All @@ -13,53 +13,45 @@ Read the [Manual](https://xl-docs.codepoems.eu)

## Supported Formats

Encode to **JPEG XL, AVIF, WEBP, and JPG**. Convert from **HEIF** and [more](https://xl-docs.codepoems.eu/supported-formats)
Encode to **JPEG XL, AVIF, WebP, and JPEG**. Convert from **HEIF, TIFF,** and [more](https://xl-docs.codepoems.eu/supported-formats)

## Features
#### Out of the Box

Just drop your images and convert. XL Converter works out of the box with no setup or steep learning curve. It prioritizes user experience while granting access to cutting-edge technology.

#### Parallel Encoding

Encode images in parallel to speed up the process. Control how much CPU to use during encoding.

#### JPG Reconstruction

Losslessly transcode JPG to JPEG XL, and reverse the process when needed.

#### Image Proxy

Avoid picky encoders. A proxy is generated when an encoder doesn't support a specific format.

For example, this enables HEIF -> JPEG XL conversion.

#### Downscaling
#### JPEGLI

Scale down images to resolution, percent, shortest (and longest) side, or file size.
Generate fully compatible JPEGs with up to [35% better compression ratio](https://opensource.googleblog.com/2024/04/introducing-jpegli-new-jpeg-coding-library.html).

#### Smallest Lossless
#### JPEG XL and AVIF

Utilize multiple formats to achieve the smallest size.
Achieve exceptional quality at a modest size with JPEG XL and AVIF.

#### Intelligent Effort
#### Parallel Encoding

Optimize `Effort` for smaller sizes.
Encode images in parallel to speed up the process. Control how much CPU to use during encoding.

#### Metadata
#### Lossless JPEG Recompression

Easily copy and wipe metadata using encoder parameters or ExifTool.
Losslessly transcode JPEG to JPEG XL, and reverse the process when needed.

#### JPEGLI
#### Downscaling

Generate the highest quality (regular old) JPGs with JPEGLI.
Scale down images to resolution, percent, shortest (and longest) side, or even file size.

## Bug Reports

You can submit a bug report in 2 ways
- \[public\] Submit a new [GitHub Issue](https://github.com/JacobDev1/xl-converter/issues)
- \[private\] Email me at [email protected]

### Sharing Files

You can share logs and images with me when making a bug report.

Upload files to a service like [Disroot Lufi](https://upload.disroot.org/) and send me a download link to [email protected]

## Contributions

Pull requests are ignored to avoid licensing issues when reusing the code.
Expand Down Expand Up @@ -114,7 +106,7 @@ Install packages.

```bash
sudo apt update
sudo apt install git make
sudo apt install git make curl
```

Install [xcb QPA](https://doc.qt.io/qt-6/linux-requirements.html) dependencies.
Expand All @@ -135,7 +127,7 @@ Build and setup Python `3.11.9`.

```bash
pyenv install 3.11.9
pyenv local 3.11.9
pyenv global 3.11.9
```

Clone and set up the repo.
Expand Down Expand Up @@ -164,19 +156,15 @@ pip install -r requirements.txt
Now, you can run it.

```bash
make run
python main.py
```

...or build it.
or build it.

```bash
make build
python build.py
```

Extra building modes:
- `make build-7z` - package to a 7z file (with an installer) (requires `p7zip-full`)
- `make build-appimage` - package as an AppImage (requires `fuse`)

### Providing Tool Binaries

To build XL Converter, you need to provide various binaries. This can be quite challenging.
Expand All @@ -190,7 +178,7 @@ Binaries needed:
- [libavif](https://github.com/AOMediaCodec/libavif) `1.0.4` (AOM `3.8.2`)
- avifenc
- avifdec
- [imagemagick](https://imagemagick.org/) `7.1.1-15 Q16-HDRI`
- [imagemagick](https://imagemagick.org/) `7.* Q16-HDRI`
- magick - AppImage for Linux
- magick.exe - Windows
- [exiftool](https://exiftool.org/) `12.77`
Expand All @@ -202,7 +190,7 @@ Place them in the following directories:
- `xl-converter\bin\win` for Windows (x86_64)
- `xl-converter/bin/linux` for Linux (x86_64)

All binaries are built statically. The version numbers should match. Binaries on Windows have an `.exe` extension.
All binaries should be statically linked.

> [!TIP]
> See the official [XL Converter builds](https://github.com/JacobDev1/xl-converter/releases) for examples.
Expand Down Expand Up @@ -236,9 +224,11 @@ Run tests
python test.py
```

You can control which tests to run. Learn more with `python test.py --help`.

### Deprecated

`test_old.py` is a deprecated, but still accessible test suite focusing on the conversion results.
`test_old.py` is a deprecated, but still accessible test suite focusing on conversion results.

```bash
python test_old.py
Expand Down
101 changes: 51 additions & 50 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@ def move(src, dst):
except OSError as err:
print(f"[Error] Moving failed ({src} -> {dst}) ({err})")

def copyTree(src, dst):
src = os.path.normpath(src)
dst = os.path.normpath(dst)

try:
shutil.copytree(src, dst, dirs_exist_ok=True)
except OSError as err:
print(f"[Error] Copying tree failed ({src} -> {dst}) ({err})")

def makedirs(path):
path = os.path.normpath(path)

Expand Down Expand Up @@ -134,20 +125,16 @@ class Args():
def __init__(self):
self.parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter)
self.args = {}
self.parser.add_argument("--app-image", "-a", help="package as an AppImage (Linux only)", action="store_true")
self.parser.add_argument("--pack", "-p", help="package to a 7z (Linux only)", action="store_true")
self.parser.add_argument("--build-type", "-b", help="Defines how to package the binaries. If not specified, vanilla build will be generated.\nPossible values: installer|portable", action="store")
self.parser.add_argument("--update-file", "-u", help="Append an update file (to place on a server).", action="store_true")

self._parseArgs()

def _parseArgs(self):
args = self.parser.parse_args()
self.args["app_image"] = args.app_image
self.args["pack"] = False if args.app_image else args.pack
self.args["build_type"] = args.build_type
self.args["update_file"] = args.update_file

if platform.system() != "Linux":
args_app_image = False
args_pack = False

def getArg(self, arg):
return self.args[arg]

Expand All @@ -173,9 +160,11 @@ def __init__(self):
"Linux": "misc/install.sh"
}

self.misc_path = (
self.assets = (
"LICENSE.txt",
"LICENSE_3RD_PARTY.txt"
"LICENSE_3RD_PARTY.txt",
"fonts/",
"sounds/",
)

# Assets
Expand Down Expand Up @@ -222,17 +211,29 @@ def build(self):
self._prepare()
self._buildBinaries()
self._copyDependencies()
self._appendInstaller()
self._appendDesktopEntry()
self._appendMisc()
self._downloadRedistributable()
self._appendUpdateFile()
self._copyAssets()
self._finish()

if self.args.getArg("app_image"):
self._buildAppImage()
elif self.args.getArg("pack"):
self._build7z()
match platform.system():
case "Linux":
match self.args.getArg("build_type"):
case "installer":
self._appendDesktopEntry()
self._appendInstaller()
self._build7z()
case "portable":
self._appendDesktopEntry()
self._buildAppImage()
case "Windows":
self.downloader.downloadRedistributable()
match self.args.getArg("build_type"):
case "installer":
self._appendInstaller()
case "portable":
print("[Error] Portable build is unavailable on Windows.")

if self.args.getArg("update_file"):
self._appendUpdateFile()

def _prepare(self):
rmTree(self.dst_dir)
Expand All @@ -247,7 +248,7 @@ def _prepare(self):
if last_platform == f"{platform.system()}_{platform.architecture()}":
print("[Building] Using previously compiled cache")
else:
print("[Building] Platform mismatch - deleting the cache")
print("[Error] Platform mismatch - deleting the cache")
rmTree("build")
rmTree("__pycache__")
else:
Expand All @@ -266,22 +267,19 @@ def _buildBinaries(self):
def _copyDependencies(self):
print("[Building] Copying dependencies")
bin_dir = self.bin_dir[platform.system()]
copyTree(bin_dir, f"{self.internal_dir}/{bin_dir}")
shutil.copytree(Path(bin_dir), Path(self.internal_dir, bin_dir))

def _appendInstaller(self):
installer_dir = self.installer_path[platform.system()]
installer_file = os.path.basename(installer_dir)

print("[Building] Appending an installer script")
match platform.system():
case "Linux":
if self.args.getArg("app_image") == False:
print("[Building] Appending an installer script")
copy(installer_dir, self.dst_dir)
if self.args.getArg("app_image") == False:
print("[Building] Embedding version into an installer script")
replaceLine(f"{self.dst_dir}/{installer_file}", "VERSION=", f"VERSION=\"{VERSION}\"\n")
copy(installer_dir, self.dst_dir)
print("[Building] Embedding version into an installer script")
replaceLine(f"{self.dst_dir}/{installer_file}", "VERSION=", f"VERSION=\"{VERSION}\"\n")
case "Windows":
print("[Building] Appending an installer script")
copy(installer_dir, self.dst_dir)
print("[Building] Embedding version into an installer script")
replaceLine(f"{self.dst_dir}/{installer_file}", "#define MyAppVersion", f"#define MyAppVersion \"{VERSION}\"\n")
Expand All @@ -292,19 +290,19 @@ def _appendDesktopEntry(self):
print("[Building] Appending a desktop entry")
copy(self.desktop_entry_path, self.dst_dir)

def _appendMisc(self):
def _copyAssets(self):
print("[Building] Appending assets")
for i in self.misc_path:
copy(i, self.internal_dir)

# Most assets
for i in self.assets:
if os.path.isdir(Path(i)):
shutil.copytree(Path(i), Path(self.internal_dir, Path(i).name))
elif os.path.isfile(Path(i)):
copy(i, self.internal_dir)

# Icons
makedirs(f"{self.internal_dir}/icons")
copy(self.icon_svg_path, f"{self.internal_dir}/icons/{os.path.basename(self.icon_svg_path)}")
copyTree(self.fonts_path, f"{self.internal_dir}/fonts")

def _downloadRedistributable(self):
if platform.system() != "Windows":
return

self.downloader.downloadRedistributable()

def _appendUpdateFile(self):
print("[Building] Appending an update file (to place on a server)")
Expand Down Expand Up @@ -345,14 +343,17 @@ def _buildAppImage(self):
subprocess.run((self.appimagetool_path, appdir, f"{self.dst_dir}/{self.build_appimage_name}"))

def _build7z(self):
if platform.system() != "Linux":
return

dst_direct = self.build_7z_name
dst = f"{self.dst_dir}/{self.build_7z_name}"
makedirs(dst)

move(f"{self.dst_dir}/{self.project_name}", dst)
move(f"{self.dst_dir}/{os.path.basename(self.installer_path['Linux'])}", dst)
move(f"{self.dst_dir}/{os.path.basename(self.desktop_entry_path)}", dst)
subprocess.run(("7z", "a", f"{dst_direct}.7z", dst_direct), cwd=self.dst_dir)
subprocess.run(("7z", "a", "-snl" , f"{dst_direct}.7z", dst_direct), cwd=self.dst_dir)

def _verifyTools(self):
match platform.system():
Expand All @@ -372,8 +373,8 @@ def _verifyTools(self):
builder = Builder()
builder.build()
except (KeyboardInterrupt, SystemExit):
print("[Building] Interrupted")
print("[Canceled] Interrupted")
exit()
except (Exception, OSError) as err:
print(f"[Building] Error - ({err})")
print(f"[Error] {err}")
exit()
33 changes: 23 additions & 10 deletions core/conflicts.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
from core.exceptions import GenericException
import re

def checkForConflicts(ext: str, file_format: str, downscaling=False) -> bool:
from data.constants import (
IMAGE_MAGICK_PATH,
)
from core.process import runProcessOutput
from core.exceptions import GenericException, FileException

def checkForConflicts(ext: str, file_format: str, downscaling=False) -> None:
"""
Raises exceptions and returns True If any conflicts occur.
Checks for conflicts with animated images. Raises exceptions and returns True If any conflicts occur.
Args:
- ext - extension (without a dot in the beginning and lowercase)
Expand All @@ -15,20 +21,27 @@ def checkForConflicts(ext: str, file_format: str, downscaling=False) -> bool:
# Animation
match ext:
case "gif":
if file_format in ("JPEG XL", "WEBP", "PNG"):
if file_format in ("JPEG XL", "WebP"):
conflict = False
case "apng":
if file_format in ("JPEG XL"):
conflict = False

if conflict:
raise GenericException("CF0", f"Animation is not supported for {ext.upper()} -> {file_format}")
raise GenericException("CF0", f"{ext.upper()} -> {file_format} conversion is not supported")

# Downscaling
if downscaling:
conflict = True
raise GenericException("CF1", f"Downscaling is not supported for animation")
else:
conflict = False

return conflict

def checkForMultipage(src_ext: str, src_abs_path: str) -> None:
"""Raises an exception if an image is multipage."""
if src_ext in ("tif", "tiff", "heif", "heic"):
try:
layers_re = re.search(r"\d+", runProcessOutput(IMAGE_MAGICK_PATH, "identify", "-format", "%n\n", src_abs_path).decode("utf-8"))
layers_n = int(layers_re.group(0))
except Exception:
raise FileException("CF2", "Cannot detect the number of pages.")

if layers_n != 1:
raise FileException("CF3", "Multipage images are not supported.")
Loading

0 comments on commit ad7c67b

Please sign in to comment.