Skip to content

Commit

Permalink
πŸ”€ Merge pull request #29 from TimNekk/develop (v1.5.0)
Browse files Browse the repository at this point in the history
πŸ› Fix a lot of Bugs
  • Loading branch information
TimNekk authored Nov 23, 2024
2 parents aa41520 + 4659192 commit 7396765
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 141 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
python-version: ['3.6','3.7','3.8','3.9','3.10']
python-version: ['3.8']

steps:
- uses: actions/checkout@v2
Expand Down
41 changes: 14 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,54 +24,41 @@

## Requirements

[Topaz Gigapixel AI](https://www.topazlabs.com/gigapixel-ai) **v6.1.0** or **newer** required
Tested on [Topaz Gigapixel AI](https://www.topazlabs.com/gigapixel-ai) **v7.2.3**

## Installation

Install the current version with [PyPI](https://pypi.org/project/gigapixel/)
Install the latest version with [PyPI](https://pypi.org/project/gigapixel/)

```bash
pip install -U gigapixel
```

## Usage

1. Create `Gigapixel` instance
2. Use `.process()` method to enhance image

```python
from gigapixel import Gigapixel, Scale, Mode, OutputFormat
from pathlib import Path

# Path to Gigapixel executable file.
exe_path = Path('C:\Program Files\Topaz Labs LLC\Topaz Gigapixel AI\Topaz Gigapixel AI.exe')

# Output file suffix. (e.g. pic.jpg -> pic-gigapixel.jpg)
# You should set same value inside Gigapixel (File -> Preferences -> Default filename suffix).
output_suffix = '-gigapixel'

# Create Gigapixel instance.
app = Gigapixel(exe_path, output_suffix)
from gigapixel import Gigapixel

# Process image.
image = Path('path/to/image.jpg')
output_path = app.process(image)
gp = Gigapixel(r"C:\Program Files\Topaz Labs LLC\Topaz Gigapixel AI\Topaz Gigapixel AI.exe")

# Print output path.
print(output_path)
gp.process(r"path\to\image.jpg")
```

Additional parameters can be passed to `process()` method **(Takes additional time)**:
Additional parameters can be passed to `process()` method:
```python
from gigapixel import Scale, Mode, OutputFormat
from gigapixel import Scale, Mode

output_path = app.process(image, scale=Scale.X2, mode=Mode.STANDARD, delete_from_history=True, output_format=OutputFormat.PNG)
gp.process(
r"path\to\image.jpg",
scale=Scale.X2,
mode=Mode.STANDARD,
)
```

> **Warning!**
> Using parameters (`scale`, `mode`, `output_format`, `delete_from_history`) will take **additional time** to process single image.
> Using parameters (`scale`, `mode`) may take **additional time** to process single image.
> Consider using them only when needed.
> To get the best performance, use `app.process(image)`
> To get the best performance, use `gp.process(r"path\to\image.jpg")`

## Contributing
Expand Down
2 changes: 1 addition & 1 deletion gigapixel/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .gigapixel import Gigapixel, Mode, Scale, OutputFormat
from .gigapixel import Gigapixel, Mode, Scale
from .exceptions import NotFile, FileAlreadyExists, GigapixelException, ElementNotFound
178 changes: 67 additions & 111 deletions gigapixel/gigapixel.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,48 @@
import os
from enum import Enum
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, Union
from pathlib import Path
import win32api
import win32con

from .logging import log, Level
from .exceptions import NotFile, FileAlreadyExists, ElementNotFound
from .exceptions import NotFile, ElementNotFound

from pywinauto import ElementNotFoundError, timings
import clipboard
from loguru import logger
from pywinauto.application import Application, ProcessNotFoundError
from pywinauto.keyboard import send_keys
from pywinauto.timings import TimeoutError
from the_retry import retry


class Scale(Enum):
X05 = "0.5x"
X1 = "1x"
X2 = "2x"
X4 = "4x"
X6 = "6x"


class Mode(Enum):
STANDARD = "Standard"
Lines = "Lines"
HIGH_FIDELITY = "High fidelity"
LOW_RESOLUTION = "Low res"
TEXT_AND_SHAPES = "Text & shapes"
ART_AND_CG = "Art & CG"
HIGH_QUALITY = "HQ"
LOW_RESOLUTION = "Low Res"
VERY_COMPRESSED = "Very Compressed"


class OutputFormat(Enum):
PRESERVE_SOURCE_FORMAT = "Preserve Source Format"
JPG = "JPG"
JPEG = "JPEG"
TIF = "TIF"
TIFF = "TIFF"
PNG = "PNG"
DNG = "DNG"
RECOVERY = "Recovery"


class Gigapixel:
def __init__(self,
executable_path: Path,
output_suffix: str,
processing_timeout: int = 900):
executable_path: Union[Path, str],
processing_timeout: int = 900) -> None:
"""
:param executable_path: Path to the executable (Topaz Gigapixel AI.exe)
:param output_suffix: Suffix to be added to the output file name (e.g. pic.jpg -> pic-gigapixel.jpg)
:param processing_timeout: Timeout for processing in seconds
"""
self._executable_path = executable_path
self._output_suffix = output_suffix
if isinstance(executable_path, str):
self._executable_path = Path(executable_path)

instance = self._get_gigapixel_instance()
self._app = self._App(instance, processing_timeout)
Expand All @@ -67,46 +59,62 @@ def __init__(self, app: Application, processing_timeout: int):
self.mode: Optional[Mode] = None

self._cancel_processing_button: Optional[Any] = None
self._delete_button: Optional[Any] = None
self._output_combo_box: Optional[Any] = None
self._preserve_source_format_button: Optional[Any] = None
self._jpg_button: Optional[Any] = None
self._jpeg_button: Optional[Any] = None
self._tif_button: Optional[Any] = None
self._tiff_button: Optional[Any] = None
self._png_button: Optional[Any] = None
self._dng_button: Optional[Any] = None
self._save_button: Optional[Any] = None
self._scale_buttons: Dict[Scale, Any] = {}
self._mode_buttons: Dict[Mode, Any] = {}

@retry(
expected_exception=(ElementNotFoundError,),
attempts=5,
backoff=0.5,
exponential_backoff=True,
)
@log("Opening photo: {}", "Photo opened", format=(1,), level=Level.DEBUG)
def open_photo(self, photo_path: Path) -> None:
while photo_path.name not in self._main_window.element_info.name:
logger.debug("Trying to open photo")
self._main_window.set_focus()
send_keys('{ESC}^o')
clipboard.copy(str(photo_path))
send_keys('^v {ENTER}')
send_keys('^v {ENTER}{ESC}')


@log("Saving photo", "Photo saved", level=Level.DEBUG)
def save_photo(self, output_format: Optional[OutputFormat]) -> None:
send_keys('^S')

if output_format:
self._set_output_format(output_format)
def save_photo(self) -> None:
self._open_export_dialog()

send_keys('{ENTER}')
if self._cancel_processing_button is None:
self._cancel_processing_button = self._main_window.child_window(title="Cancel Processing",
self._cancel_processing_button = self._main_window.child_window(title="Close window",
control_type="Button",
depth=1)
self._cancel_processing_button.wait_not('visible', timeout=self._processing_timeout)

@log("Deleting photo from history", "Photo deleted", level=Level.DEBUG)
def delete_photo(self) -> None:
if self._delete_button is None:
self._delete_button = self._main_window.Pane.Button2
self._delete_button.click_input()
self._cancel_processing_button.wait('visible', timeout=self._processing_timeout)

self._close_export_dialog()

@retry(
expected_exception=(TimeoutError,),
attempts=10,
backoff=0.1,
exponential_backoff=True,
)
@log("Opening export dialog", "Export dialog opened", level=Level.DEBUG)
def _open_export_dialog(self) -> None:
send_keys('^S')
if self._save_button is None:
self._save_button = self._main_window.child_window(title="Save", control_type="Button", depth=1)
self._save_button.wait('visible', timeout=0.1)

@retry(
expected_exception=(TimeoutError,),
attempts=10,
backoff=0.1,
exponential_backoff=True,
)
@log("Closing export dialog", "Export dialog closed", level=Level.DEBUG)
def _close_export_dialog(self) -> None:
send_keys('{ESC}')
self._cancel_processing_button.wait_not('visible', timeout=0.1)

@log("Setting processing options", "Processing options set", level=Level.DEBUG)
def set_processing_options(self, scale: Optional[Scale] = None, mode: Optional[Mode] = None) -> None:
Expand All @@ -115,47 +123,6 @@ def set_processing_options(self, scale: Optional[Scale] = None, mode: Optional[M
if mode:
self._set_mode(mode)

def _set_output_format(self, save_format: OutputFormat) -> None:
if self._output_combo_box is None:
self._output_combo_box = self._main_window.ComboBox
self._output_combo_box.click_input()

if save_format == OutputFormat.PRESERVE_SOURCE_FORMAT:
if self._preserve_source_format_button is None:
self._preserve_source_format_button = self._main_window.ListItem
self._preserve_source_format_button.click_input()
send_keys('{TAB}')
elif save_format == OutputFormat.JPG:
if self._jpg_button is None:
self._jpg_button = self._main_window.ListItem2
self._jpg_button.click_input()
send_keys('{TAB}')
elif save_format == OutputFormat.JPEG:
if self._jpeg_button is None:
self._jpeg_button = self._main_window.ListItem3
self._jpeg_button.click_input()
send_keys('{TAB}')
elif save_format == OutputFormat.TIF:
if self._tif_button is None:
self._tif_button = self._main_window.ListItem4
self._tif_button.click_input()
send_keys('{TAB} {TAB} {TAB}')
elif save_format == OutputFormat.TIFF:
if self._tiff_button is None:
self._tiff_button = self._main_window.ListItem5
self._tiff_button.click_input()
send_keys('{TAB} {TAB} {TAB}')
elif save_format == OutputFormat.PNG:
if self._png_button is None:
self._png_button = self._main_window.ListItem6
self._png_button.click_input()
send_keys('{TAB}')
elif save_format == OutputFormat.DNG:
if self._dng_button is None:
self._dng_button = self._main_window.ListItem7
self._dng_button.click_input()
send_keys('{TAB}')

def _set_scale(self, scale: Scale):
if self.scale == scale:
return
Expand Down Expand Up @@ -202,51 +169,40 @@ def _open_topaz(self) -> Application:
return instance

@log("Checking path: {}", "Path is valid", format=(1,), level=Level.DEBUG)
def _check_path(self, path: Path, output_format: Optional[OutputFormat]) -> None:
def _check_path(self, path: Path) -> None:
if not path.is_file():
raise NotFile(f"Path is not a file: {path}")

save_path = self._get_save_path(path, output_format)
if save_path.name in os.listdir(path.parent):
raise FileAlreadyExists(f"Output file already exists: {save_path}")

@staticmethod
def _remove_suffix(input_string: str, suffix: str) -> str:
if suffix and input_string.endswith(suffix):
return input_string[:-len(suffix)]
return input_string

def _get_save_path(self, path: Path, output_format: Optional[OutputFormat]) -> Path:
extension = path.suffix if output_format is None or output_format == OutputFormat.PRESERVE_SOURCE_FORMAT else \
f".{output_format.value.lower()}"
return path.parent / (Gigapixel._remove_suffix(path.name, path.suffix) + self._output_suffix + extension)

def _set_english_layout(self) -> None:
english_layout = 0x0409
win32api.LoadKeyboardLayout(hex(english_layout), win32con.KLF_ACTIVATE)

@log(start="Starting processing: {}", format=(1,))
@log(end="Finished processing: {}", format=(1,), level=Level.SUCCESS)
def process(self,
photo_path: Path,
photo_path: Union[Path, str],
scale: Optional[Scale] = None,
mode: Optional[Mode] = None,
delete_from_history: bool = False,
output_format: Optional[OutputFormat] = None
) -> Path:
) -> None:
"""
Process a photo using Topaz Gigapixel AI
:param photo_path: Path to the photo to be processed
:param scale: Scale to be used for processing
:param mode: Mode to be used for processing
:param delete_from_history: Whether to delete the photo from history after processing
:param output_format: Output format of the processed photo
:return: Path to the processed photo
"""
self._check_path(photo_path, output_format)
if isinstance(photo_path, str):
photo_path = Path(photo_path)

self._set_english_layout()
self._check_path(photo_path)

self._app.open_photo(photo_path)
self._app.set_processing_options(scale, mode)
self._app.save_photo(output_format)

if delete_from_history:
self._app.delete_photo()

return self._get_save_path(photo_path, output_format)
self._app.save_photo()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pywinauto
clipboard
loguru
loguru
the-retry

0 comments on commit 7396765

Please sign in to comment.