Skip to content

Commit

Permalink
ADDED: SUPPORT FOR MASK MANIPULATION
Browse files Browse the repository at this point in the history
  • Loading branch information
OfficialAhmed committed Jul 4, 2023
1 parent 3d979a6 commit f09e8e6
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 91 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ output
.vs
.vscode
*.zip
test.py
47 changes: 25 additions & 22 deletions Module/Icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import sys
from environment import Common

from PyQt5.QtWidgets import QFileDialog
Expand Down Expand Up @@ -90,36 +89,37 @@ def is_valid_image(self, image_type, image_path) -> bool:

match image_type:
case "icon":
required_dimensions = self.constant.get_ps4_icon_size()
limit_dimenstions = (1080, 720)
most_dimenstions = (1080, 720)
least_dimensions = self.constant.get_ps4_icon_size()
background = ""

case "picture":
required_dimensions = self.constant.get_ps4_pic_size()
limit_dimenstions = (2040, 1920)
most_dimenstions = (2040, 1920)
least_dimensions = self.constant.get_ps4_pic_size()
background = f"{self.pref_path}Black.@OfficialAhmed0"

image_dimension = Image.open(image_path).size
icon_dimensions = Image.open(image_path).size
icon_x, icon_y = icon_dimensions[0], icon_dimensions[1]

# Correct size
if image_dimension == required_dimensions:
self.change_dimensions_label(self.constant.get_color("green"), background, size=str(image_dimension))
if icon_dimensions == least_dimensions:
self.change_dimensions_label(self.constant.get_color("green"), background, size=str(icon_dimensions))
self.logging += self.html.internal_log_msg("success", self.translated_logs.get("CorrectDim"))
is_valid = True

# Less than the required [CANNOT BE RESIZED]
elif image_dimension[0] < required_dimensions[0] or image_dimension[1] < required_dimensions[1]:
self.change_dimensions_label(self.constant.get_color("red"), size = str(image_dimension))
elif icon_x < least_dimensions[0] or icon_y < least_dimensions[1]:
self.change_dimensions_label(self.constant.get_color("red"), size = str(icon_dimensions))
self.logging += self.html.internal_log_msg("error", self.translated_logs.get("SmallDim"))

# Greater than the limit [CANNOT BE RESIZED]
elif image_dimension[0] > limit_dimenstions[0] or image_dimension[1] > limit_dimenstions[1]:
self.change_dimensions_label(self.constant.get_color("red"), size = str(image_dimension))
self.logging += self.html.internal_log_msg("error", f"{self.translated_logs.get('ErrDim')} {limit_dimenstions}")
elif icon_x > most_dimenstions[0] or icon_y > most_dimenstions[1]:
self.change_dimensions_label(self.constant.get_color("red"), size = str(icon_dimensions))
self.logging += self.html.internal_log_msg("error", f"{self.translated_logs.get('ErrDim')} {most_dimenstions}")

# Otherwise [CAN BE RESIZED]
else:
self.change_dimensions_label(self.constant.get_color("orange"), background, str(image_dimension))
self.change_dimensions_label(self.constant.get_color("orange"), background, str(icon_dimensions))
self.logging += self.html.internal_log_msg("warning", self.translated_logs.get('LargeDim'))
is_valid = True

Expand All @@ -144,28 +144,31 @@ def refresh_ui(self, is_from_dropdown_list:bool = False) -> None:
icon_index = f"{self.GameTitles.currentIndex() + 1}"

match self.selected_mode:

case "game":

if self.is_toggled_homebrew == "True":
if "CUSA" not in self.current_game_id:
hb = self.translated_content.get("HomebrewLabel_Y")
else:
hb = self.translated_content.get("HomebrewLabel_N")
hb_label = "HomebrewLabel_Y" if "CUSA" not in self.current_game_id else "HomebrewLabel_N"
else:
hb = self.translated_content.get("HomebrewLabel_T")
hb_label = "HomebrewLabel_T"


if self.ids.get(self.current_game_id).get("location").upper() == "INTERNAL":
location = self.translated_content.get("IconLocation_In")
icon_location = "In"
else:
location = self.translated_content.get("IconLocation_Ex")
icon_location = "Ex"

hb = self.translated_content.get(hb_label)
location = self.translated_content.get(f"IconLocation_{icon_location}")

self.IconLocationTxt.setText(location)
self.GameTitleLabel.setText(self.ids.get(self.current_game_id).get("title"))


case "system apps":
hb = self.translated_content.get("HomebrewLabel_Sys")
self.GameTitleLabel.setText(self.ids.get(self.current_game_id))


self.HomebrewLabel.setText(hb)
self.GameIdTxt.setText(self.current_game_id)
self.TotalGamesTxt.setText(f"{icon_index}/{self.icons_limit}")
Expand Down
180 changes: 111 additions & 69 deletions Module/Mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,50 @@ def __init__(self) -> None:

win_name = "MaskMakerWindow"
self.translated_content: dict = self.translation.get_translation(self.language, win_name)


def quit(self):
exit()


def show_mask(self) -> None:
self.MaskView.setStyleSheet(f"border-image: url({self.mask_location}mask-style.png);")


def show_baked_icon(self):
path = self.temp_path.replace('\\', '/')
self.BakedView.setStyleSheet(f"border-image: url({path}preview.png);")


def bake_preview_icon(self) -> None:
"""
Bake a temp icon for preview with chosen mask
"""

style = Image.open(f"{self.temp_path}mask-style.png")
cover = Image.open(f"{self.temp_path}mask.png").resize(self.ps4_icon_dimension).convert("L")
mask = style.copy()

with Image.open(f"{self.preview_icon_path}previewTest.@OfficialAhmed0") as icon:

mask_copy = mask.copy()
mask_copy.paste(icon, (0, 0), cover)
mask_copy.save(f"{self.temp_path}preview.png")


def validate_baking(self) -> None:
"""
Enable/Disable the baking button
"""

enable = False
if self.group_icons_is_changed and self.mask_is_changed:

self.bake_preview_icon()
self.show_baked_icon()
enable = True

self.BakeBtn.setEnabled(enable)


def check_bake_state(self):
Expand Down Expand Up @@ -116,7 +160,7 @@ def browse_mask(self) -> None:

mask_location, _ = QtWidgets.QFileDialog.getOpenFileName(
None,
"Choose a mask for the icon",
"CHOOSE MASK FOR THE ICONS",
self.last_browse_path,
"Zip(*.zip)",
options=options,
Expand All @@ -130,7 +174,6 @@ def browse_mask(self) -> None:
if os.path.getsize(mask_location) <= 120000:

try:

# Check the unpacked zip, if compatible and contain masks
shutil.unpack_archive(mask_location, self.temp_path, "zip")

Expand Down Expand Up @@ -161,8 +204,9 @@ def browse_mask(self) -> None:

def revert_to_default(self):
"""
Get selected group to default style from default cached icons
*Prompt user before proceeding*
Get selected group to `default style` from default cached icons
* ##### User will be prompted before proceeding
"""

translated_content: dict = self.translated_content.get("RevertToDefaultWindow")
Expand All @@ -181,95 +225,93 @@ def revert_to_default(self):

# read from default cached icons -> generate No of icons (png/dds) -> upload to PS4



def bake_mask(self) -> None:

def apply_mask(self, id, cover, mask:Image, lock):
"""
Apply chosen mask on all JSON Group
- Shrink icon while keeping aspect ratio `(512, 512)`
by pasting the icon/mask on transparent image
- i.e. shrink size `(412, 412)` - trasparent `(100, 100)` - icon `(512, 512)`
- Apply mask on style according to the cover(B&W photo)
"""

def apply_mask(id, cover, mask:Image, lock):
"""
Apply mask on style according to the cover(B&W photo)
"""

with Image.open(f"{self.temp_path}Groups\\Backup\\{id}.png").resize(self.ps4_icon_dimension) as icon:

mask_copy = mask.copy()
mask_copy.paste(icon, (0, 0), cover)

# One thread writing to the system at a time
with lock:
mask_copy.save(f"{self.baked_path}{id}.png")
vertical_shift = 0
horizontal_shift = 0
width, height = self.ps4_icon_dimension

game_icon = Image.open(f"{self.temp_path}Groups\\Backup\\{id}.png")
transparent_icon = Image.new("RGBA", self.ps4_icon_dimension, (0, 0, 0, 0))

try:
# Reading mask specifications from JSON
with open(f"{self.temp_path}set.json") as file:
info = json.load(file)

# Lock for all threads before race conditions or other synchronization issues
lock = Lock()
vertical_shift = info.get("vertical_shift")
horizontal_shift = info.get("horizontal_shift")
width, height = info.get("width"), info.get("height")
except: pass

with concurrent.futures.ThreadPoolExecutor() as executor:
resized_game_icon = game_icon.resize((width, height))
resized_game_icon_x = resized_game_icon.size[0]
resized_game_icon_y = resized_game_icon.size[1]

style = Image.open(f"{self.temp_path}mask-style.png")
cover = Image.open(f"{self.temp_path}mask.jpg").resize(self.ps4_icon_dimension).convert("L")
mask = style.copy()
if vertical_shift or horizontal_shift:

# Apply mask for all ids
tasks = [executor.submit(apply_mask, id, cover, mask, lock) for id in self.group_ids]

concurrent.futures.wait(tasks)
# Set Shift to 0 if not included in the mask
vertical_shift = 0 if not vertical_shift else vertical_shift
horizontal_shift = 0 if not horizontal_shift else horizontal_shift

self.BakeState.setText("DONE!")
self.UploadBtn.setEnabled(True)

except Exception as e:
position = ((horizontal_shift, vertical_shift))

else:

self.BakeState.setText("Error baking mask, read logs.txt")
self.log_to_external_file(str(e), "Error")
# Calculate the center point to center-align the shrunken icon
center_point = ( (512 - resized_game_icon_x) // 2, (512 - resized_game_icon_y) // 2 )
position = center_point

finally:
transparent_icon.paste(resized_game_icon, position)

# with Image.open(open(transparent_icon)) as icon:
mask_copy = mask.copy()
mask_copy.paste(transparent_icon, (0, 0), cover)

with lock:

self.BakeBtn.setEnabled(False)
# One thread writing to the system at a time
mask_copy.save(f"{self.baked_path}{id}.png")


def bake_preview_icon(self) -> None:
"""
Bake a temp icon for preview with chosen mask
def bake_mask(self) -> None:
"""
#### Low-level implementation
style = Image.open(f"{self.temp_path}mask-style.png")
cover = Image.open(f"{self.temp_path}mask.jpg").resize(self.ps4_icon_dimension).convert("L")
mask = style.copy()

with Image.open(f"{self.preview_icon_path}previewTest.@OfficialAhmed0") as icon:

mask_copy = mask.copy()
mask_copy.paste(icon, (0, 0), cover)
mask_copy.save(f"{self.temp_path}preview.png")


def validate_baking(self) -> None:
"""
Enable/Disable the baking button
* Baking icon & mask concurrently using Threads
"""

enable = False
if self.group_icons_is_changed and self.mask_is_changed:

self.bake_preview_icon()
self.show_baked_icon()
enable = True
try:

self.BakeBtn.setEnabled(enable)
# Lock - Restrict to one thread at a time writing to memory to prevent race conditions/synchronization issues
lock = Lock()

with concurrent.futures.ThreadPoolExecutor() as executor:

def show_mask(self) -> None:
self.MaskView.setStyleSheet(f"border-image: url({self.mask_location}mask-style.png);")
style = Image.open(f"{self.temp_path}mask-style.png")
cover = Image.open(f"{self.temp_path}mask.png").convert("L")
mask = style.copy()

# Apply mask for all ids
tasks = [executor.submit(self.apply_mask, id, cover, mask, lock) for id in self.group_ids]

concurrent.futures.wait(tasks)

def show_baked_icon(self):
path = self.temp_path.replace('\\', '/')
self.BakedView.setStyleSheet(f"border-image: url({path}preview.png);")
self.BakeState.setText("DONE!")
self.UploadBtn.setEnabled(True)

except Exception as e:
self.BakeState.setText("Error baking mask, read logs.txt")
self.log_to_external_file(str(e), "Error")

def quit(self):
exit()
finally:
self.BakeBtn.setEnabled(False)

0 comments on commit f09e8e6

Please sign in to comment.