Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
330304b
Extract memo information from PNG file Comment tag and display in Kni…
dl1com Mar 19, 2025
542ee01
Reverse image properly
t0mpr1c3 Mar 29, 2025
a1531f1
Fix tests
t0mpr1c3 Mar 29, 2025
02fbb4d
Use default memo value
t0mpr1c3 Mar 29, 2025
b3d74f4
Remove memo column from knit progress window when no memo information
t0mpr1c3 Mar 29, 2025
e4d8271
Rename variable
t0mpr1c3 Mar 29, 2025
a86dc9e
fix needle numbers in knitting progress window when memo information …
t0mpr1c3 Mar 29, 2025
c0b4c96
Update patterns submodule to include WIP converted patterns
jonathanperret Mar 31, 2025
aea8e89
Allow non-digit memo characters to be displayed
t0mpr1c3 Mar 31, 2025
2836dde
Fix type of
t0mpr1c3 Mar 31, 2025
b6f5537
Fix type of memo
t0mpr1c3 Mar 31, 2025
3b7c243
Fix type of memos
t0mpr1c3 Mar 31, 2025
8793fed
Fix type of memos
t0mpr1c3 Mar 31, 2025
9abe187
Fix type of memo
t0mpr1c3 Mar 31, 2025
7822017
Update control.py
t0mpr1c3 Mar 31, 2025
764d261
Update patterns submodule to include WIP converted patterns
jonathanperret Mar 31, 2025
2340df0
Fix fragile selection side identification in knit progress window
t0mpr1c3 Mar 31, 2025
80d09cb
Rename class Transform to class ImageTransform
t0mpr1c3 Mar 31, 2025
12b877c
Apply image transforms to Memo data
t0mpr1c3 Mar 31, 2025
14a2246
Fix exception handling in memo extraction
t0mpr1c3 Apr 20, 2025
12e40c3
Remove redundant try/excecpt clause
t0mpr1c3 Apr 20, 2025
03b6404
Remove TODO
t0mpr1c3 Apr 20, 2025
593551c
Remove TODO
t0mpr1c3 Apr 20, 2025
6f192ab
Add error handling to image transform
t0mpr1c3 Apr 20, 2025
2958126
Fix error handling in image transform
t0mpr1c3 Apr 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/main/python/main/ayab/ayab.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from .audio import AudioPlayer
from .menu import Menu
from .scene import Scene
from .transforms import Transform
from .transforms import ImageTransform
from .firmware_flash import FirmwareFlash
from .hw_test import HardwareTestDialog
from .preferences import Preferences
Expand Down Expand Up @@ -117,8 +117,8 @@ def __activate_menu(self) -> None:
self.menu.ui.action_cancel.triggered.connect(self.engine.cancel)
self.menu.ui.action_set_preferences.triggered.connect(self.__set_prefs)
self.menu.ui.action_about.triggered.connect(self.about.show)
# get names of image actions from Transform methods
transforms = filter(lambda x: x[0] != "_", Transform.__dict__.keys())
# get names of image actions from ImageTransform methods
transforms = filter(lambda x: x[0] != "_", ImageTransform.__dict__.keys())
for t in transforms:
action = getattr(self.menu.ui, "action_" + t)
slot = getattr(self.scene.ayabimage, t)
Expand Down
17 changes: 16 additions & 1 deletion src/main/python/main/ayab/engine/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class Control(SignalSender):
initial_position: int
len_pat_expanded: int
line_block: int
memos: list[str]
mode: Mode
mode_func: ModeFuncType
num_colors: int
Expand All @@ -85,14 +86,15 @@ def __init__(self, parent: GuiMain, engine: Engine):
self.api_version: int = self.FIRST_SUPPORTED_API_VERSION

def start(
self, pattern: Pattern, options: OptionsTab, operation: Operation
self, pattern: Pattern, memos: list[str], options: OptionsTab, operation: Operation
) -> None:
self.machine = options.machine
if operation == Operation.KNIT:
self.former_request = 0
self.line_block = 0
self.pattern_repeats = 0
self.pattern = pattern
self.memos = memos
self.pat_height = pattern.pat_height
self.num_colors = options.num_colors
self.start_row = options.start_row
Expand Down Expand Up @@ -232,6 +234,12 @@ def cnf_line_API6(self, line_number: int) -> bool:
+ " pat_row: "
+ str(self.pat_row)
)
try:
msg = msg + " memo: " + str(self.memos[self.pat_row])
except IndexError:
pass
except Exception as e:
self.logger.debug(f"Error logging memo: {str(e)}")
if blank_line:
msg += " BLANK LINE"
else:
Expand Down Expand Up @@ -265,6 +273,13 @@ def __update_status(self, line_number: int, color: int, bits: bitarray) -> None:
self.status.total_rows = self.pat_height
self.status.current_row = self.pat_row + 1
self.status.line_number = line_number
try:
self.status.memo = self.memos[self.pat_row]
except IndexError:
self.status.memo = "0"
except Exception as e:
self.logger.warning(f"Error setting memo value: {str(e)}")
self.status.memo = "0"
if self.inf_repeat:
self.status.repeats = self.pattern_repeats
if self.mode != Mode.SINGLEBED:
Expand Down
12 changes: 8 additions & 4 deletions src/main/python/main/ayab/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .dock_gui import Ui_Dock
from typing import TYPE_CHECKING, Literal, Optional, cast
from ..signal_sender import SignalSender
from ..image import AyabImage

if TYPE_CHECKING:
from ..ayab import GuiMain
Expand All @@ -51,6 +52,7 @@ class Engine(SignalSender, QDockWidget):

port_opener = Signal()

memos: list[str]
pattern: Pattern
status: StatusTab

Expand All @@ -67,6 +69,7 @@ def __init__(self, parent: GuiMain):
parent.ui.dock_container_layout.addWidget(self)

self.pattern: Pattern = None # type:ignore
self.memos = []
self.control = Control(parent, self)
self.__feedback = FeedbackHandler(parent)
self.__logger = logging.getLogger(type(self).__name__)
Expand Down Expand Up @@ -117,7 +120,7 @@ def __populate_ports(self, port_list: Optional[list[str]] = None) -> None:
def __read_portname(self) -> str:
return self.ui.serial_port_dropdown.currentText()

def knit_config(self, image: Image.Image) -> None:
def knit_config(self, im: AyabImage) -> None:
"""
Read and check configuration options from options dock UI.
"""
Expand All @@ -126,11 +129,12 @@ def knit_config(self, image: Image.Image) -> None:
self.__logger.debug(self.config.as_dict())

# start to knit with the bottom first
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image_rev: Image.Image = im.image.transpose(Image.FLIP_TOP_BOTTOM)

# TODO: detect if previous conf had the same
# image to avoid re-generating.
self.pattern = Pattern(image, self.config, self.config.num_colors)
self.pattern = Pattern(image_rev, im.memos, self.config, self.config.num_colors)
self.memos = im.memos

# validate configuration options
valid, msg = self.validate()
Expand Down Expand Up @@ -167,7 +171,7 @@ def run(self, operation: Operation) -> None:

# setup knitting controller
self.config.portname = self.__read_portname()
self.control.start(self.pattern, self.config, operation)
self.control.start(self.pattern, self.memos, self.config, operation)

with keep.presenting(on_fail="pass"):
while True:
Expand Down
13 changes: 9 additions & 4 deletions src/main/python/main/ayab/engine/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@


class Pattern(object):
def __init__(self, image: Image.Image, config: OptionsTab, num_colors: int = 2):
self.__pattern = (
def __init__(self, image: Image.Image, memos: list[str], config: OptionsTab, num_colors: int = 2):
self.__pattern : Image.Image = (
image.transpose(Image.FLIP_LEFT_RIGHT) if config.auto_mirror else image
)
self.__num_colors = num_colors
self.__alignment = Alignment.CENTER
self.__memos : list[str] = memos
self.__num_colors : int = num_colors
self.__alignment : Alignment = Alignment.CENTER
self.__pat_start_needle: int = -1
self.__pat_end_needle: int = -1
self.__knit_start_needle: int = 0
Expand Down Expand Up @@ -198,5 +199,9 @@ def pat_width(self) -> int:
def pattern_expanded(self) -> list[bitarray]:
return self.__pattern_expanded

@property
def memos(self) -> list[str]:
return self.__memos

def array2rgb(self, a: list[int]) -> int:
return (a[0] & 0xFF) * 0x10000 + (a[1] & 0xFF) * 0x100 + (a[2] & 0xFF)
3 changes: 3 additions & 0 deletions src/main/python/main/ayab/engine/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Status(object):
current_row: int
firmware_state: int
line_number: int
memo: str
repeats: int
total_rows: int
# carriage info
Expand All @@ -145,6 +146,7 @@ def reset(self) -> None:
self.current_row = -1
self.firmware_state = -1
self.line_number = -1
self.memo = '0'
self.repeats = -1
self.total_rows = -1
# carriage info
Expand All @@ -159,6 +161,7 @@ def copy(self, status: Status) -> None:
self.firmware_state = status.firmware_state
self.current_row = status.current_row
self.line_number = status.line_number
self.memo = status.memo
self.repeats = status.repeats
self.color_symbol = status.color_symbol
self.color = status.color
Expand Down
3 changes: 2 additions & 1 deletion src/main/python/main/ayab/gui_fsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def set_transitions(self, parent: GuiMain) -> None:
self.CONFIGURING.entered.connect(parent.menu.add_image_actions)
self.CONFIGURING.entered.connect(parent.progbar.reset)
self.CHECKING.entered.connect(
lambda: parent.engine.knit_config(parent.scene.ayabimage.image)
lambda: parent.engine.knit_config(parent.scene.ayabimage)
)
self.KNITTING.entered.connect(parent.start_knitting)
self.TESTING.entered.connect(parent.start_testing)
Expand Down Expand Up @@ -145,6 +145,7 @@ def set_properties(self, parent: GuiMain) -> None:
self.CONFIGURING.assignProperty(parent.ui.cancel_button, "enabled", "False")
self.KNITTING.assignProperty(parent.ui.cancel_button, "enabled", "True")
self.TESTING.assignProperty(parent.ui.cancel_button, "enabled", "False")

# Cancel Knitting menu action
self.NO_IMAGE.assignProperty(parent.menu.ui.action_cancel, "enabled", "False")
self.CONFIGURING.assignProperty(
Expand Down
54 changes: 40 additions & 14 deletions src/main/python/main/ayab/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
# Andreas Müller, Christian Gerbrandt
# https://github.com/AllYarnsAreBeautiful/ayab-desktop


from __future__ import annotations
import logging
from math import ceil
Expand All @@ -26,7 +27,7 @@
from PySide6.QtCore import QCoreApplication
from PySide6.QtWidgets import QInputDialog, QDialog, QFileDialog

from .transforms import Transform, Mirrors
from .transforms import ImageTransform, Mirrors
from .signal_sender import SignalSender
from .utils import display_blocking_popup
from .machine import Machine
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self, parent: GuiMain):
super().__init__(parent.signal_receiver)
self.__parent = parent
self.image: Image.Image = None # type: ignore
self.memos: list[str] = []
self.filename: Optional[str] = None
self.filename_input = self.__parent.ui.filename_lineedit

Expand Down Expand Up @@ -87,12 +89,12 @@ def __load(self, filename: str) -> None:
display_blocking_popup(
QCoreApplication.translate("Image", "Unable to load image file"),
"error",
) # FIXME translate
)
logging.error("Unable to load " + str(filename))
except Exception as e:
display_blocking_popup(
QCoreApplication.translate("Image", "Error loading image file"), "error"
) # FIXME translate
)
logging.error("Error loading image: " + str(e))
raise
else:
Expand All @@ -101,6 +103,7 @@ def __load(self, filename: str) -> None:
self.__parent.engine.config.refresh()

def __open(self, filename: str) -> None:
self.memos = []
# check for files that need conversion
suffix = filename[-4:].lower()
if suffix == ".pat":
Expand All @@ -111,12 +114,25 @@ def __open(self, filename: str) -> None:
self.image = CutPatternConverter().pattern2im(filename)
else:
self.image = Image.open(filename)
if suffix == ".png":
# check metadata for memo information
self.image.load()
if "Comment" in self.image.info and len(str(self.image.info["Comment"])) > 0:
comment = str(self.image.info["Comment"])
if comment.startswith("AYAB:"):
# update memo information
for i in range(len(comment) - 5):
self.memos.append(comment[i + 5])
# report metadata
logging.info("File metadata Comment tag: " + comment)
logging.info("File memo information: " + str(self.memos))
self.image = self.image.convert("RGBA")
self.emit_got_image_flag()
self.emit_image_resizer()

def invert(self) -> None:
self.apply_transform(Transform.invert)
self.apply_transform(ImageTransform.invert)
# memos unchanged

def repeat(self) -> None:
machine_width = Machine(self.__parent.prefs.value("machine")).width
Expand All @@ -131,7 +147,8 @@ def repeat(self) -> None:
minValue=1,
maxValue=ceil(machine_width / self.image.width),
)
self.apply_transform(Transform.repeat, v[0], h[0])
self.apply_transform(ImageTransform.repeat, v[0], h[0])
self.memos = self.memos * v[0]

def stretch(self) -> None:
machine_width = Machine(self.__parent.prefs.value("machine")).width
Expand All @@ -146,24 +163,30 @@ def stretch(self) -> None:
minValue=1,
maxValue=ceil(machine_width / self.image.width),
)
self.apply_transform(Transform.stretch, v[0], h[0])
self.apply_transform(ImageTransform.stretch, v[0], h[0])
self.memos = []
Comment on lines +166 to +167
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider preserving memo data for safe transforms

The current implementation clears memos for several transforms (stretch, reflect, vflip, rotate_left, rotate_right). According to the PR discussion, some of these might be safe in specific circumstances.

For user experience consistency, consider adding a warning dialog when a transform would clear memo data, giving users the option to proceed or cancel.

def _clear_memos_with_warning(self, transform_name):
    """Display warning and clear memos if user confirms."""
    result = display_warning_dialog(
        f"The {transform_name} transform will clear memo data. Proceed?",
        buttons=["Yes", "No"]
    )
    if result == "Yes":
        self.memos = []
        return True
    return False

# Then in transform methods like stretch:
def stretch(self):
    # ... existing code ...
    if self.memos and not self._clear_memos_with_warning("Stretch"):
        return  # User canceled
    self.apply_transform(ImageTransform.stretch, v[0], h[0])

Also applies to: 172-173, 180-181, 184-185, 188-189


def reflect(self) -> None:
m = Mirrors()
if m.result == QDialog.DialogCode.Accepted:
self.apply_transform(Transform.reflect, m.mirrors)
self.apply_transform(ImageTransform.reflect, m.mirrors)
self.memos = []

def hflip(self) -> None:
self.apply_transform(Transform.hflip)
self.apply_transform(ImageTransform.hflip)
# memos unchanged

def vflip(self) -> None:
self.apply_transform(Transform.vflip)
self.apply_transform(ImageTransform.vflip)
self.memos = []

def rotate_left(self) -> None:
self.apply_transform(Transform.rotate_left)
self.apply_transform(ImageTransform.rotate_left)
self.memos = []

def rotate_right(self) -> None:
self.apply_transform(Transform.rotate_right)
self.apply_transform(ImageTransform.rotate_right)
self.memos = []

def zoom_in(self) -> None:
self.__parent.scene.set_zoom(+1)
Expand All @@ -177,11 +200,14 @@ def apply_transform(
*args: tuple[int, int] | list[int] | int,
) -> None:
"""Executes an image transform specified by function and args."""
self.image = transform(self.image, args)
try:
pass # self.image = transform(self.image, args)
self.image = transform(self.image, args)
except Exception as e:
logging.error("Error while executing image transform: " + repr(e))
display_blocking_popup(
QCoreApplication.translate("Image", "Error applying transform"), "error"
)
logging.error(f"Error in transform {transform.__name__}: {str(e)}")
return

# Update the view
self.emit_image_resizer()
Expand Down
Loading