Skip to content

Commit

Permalink
add apple panels processor
Browse files Browse the repository at this point in the history
  • Loading branch information
willwade committed Jan 20, 2025
1 parent 90a0b5f commit a58b68b
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 79 deletions.
74 changes: 63 additions & 11 deletions aac_processors/apple_panels_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ def __init__(self) -> None:
"""Initialize the processor."""
super().__init__()
self.collected_texts = []
self.file_path: Optional[str] = None
self.original_filename: Optional[str] = None
self.original_file_path: Optional[str] = None

def can_process(self, file_path: str) -> bool:
"""Check if file is an Apple Panels config.
Expand Down Expand Up @@ -69,7 +72,7 @@ def _extract_button_info(
y = int(float(rect_parts[1]))
# Calculate grid position based on coordinates
# Assuming standard button size of 100x25
row = y // 25
row = y // 100
col = x // 100
except (ValueError, IndexError):
row, col = 0, 0
Expand Down Expand Up @@ -209,12 +212,11 @@ def save_from_tree(self, tree: AACTree, output_path: str) -> None:
with open(contents_dir / "Info.plist", "wb") as f:
plistlib.dump(info, f)

# Create empty AssetIndex.plist
with open(resources_dir / "AssetIndex.plist", "wb") as f:
plistlib.dump({}, f)
# Create AssetIndex.plist for images
assets = {}

# Create PanelDefinitions.plist
panels: dict[str, Any] = {
# Convert pages to panels
panels = {
"Panels": {},
"ToolbarOrdering": {
"ToolbarIdentifiersAfterBasePanel": [],
Expand Down Expand Up @@ -244,11 +246,28 @@ def save_from_tree(self, tree: AACTree, output_path: str) -> None:
"UsesPinnedResizing": False,
}

# Get page dimensions
rows, cols = page.grid_size
page_width = 1100 # Default width
page_height = page_width * (
rows / cols
) # Make height proportional to maintain square buttons
button_size = min(page_width / cols, page_height / rows) # Square buttons

# Convert buttons
for btn in page.buttons:
row, col = btn.position
x = col * 100 # Standard width 100
y = row * 25 # Standard height 25
# Use absolute position if available, otherwise calculate from grid
if btn.left is not None and btn.top is not None:
x = int(btn.left * page_width)
y = int(btn.top * page_height)
else:
row, col = btn.position
x = int(col * button_size)
y = int(row * button_size)

# Use square button dimensions
width = int(button_size)
height = int(button_size)

button = {
"ButtonType": 0,
Expand All @@ -261,9 +280,38 @@ def save_from_tree(self, tree: AACTree, output_path: str) -> None:
"FontSize": 12,
"ID": btn.id,
"PanelObjectType": "Button",
"Rect": f"{{{{{x}, {y}}}, {{100, 25}}}}",
"Rect": f"{{{{{x}, {y}}}, {{{width}, {height}}}}}",
}

# Handle image if present
if btn.image and btn.image.get("url"):
import uuid

# Generate unique image ID in Apple format
image_id = f"Image.{str(uuid.uuid4()).upper()}"

# Add to assets index with proper format
assets[image_id] = {
"Type": "Image",
"Name": btn.label
or "Button Image", # Use button label as image name
}

# Add image reference to button
button["DisplayImageResource"] = image_id

# Download and save image directly in Resources
try:
import requests

response = requests.get(btn.image["url"])
if response.status_code == 200:
# Save image with the same ID as referenced
with open(resources_dir / image_id, "wb") as f:
f.write(response.content)
except Exception as e:
self.debug(f"Failed to download image {btn.image['url']}: {e}")

# Add actions
if btn.type == ButtonType.NAVIGATE and btn.target_page_id:
button["Actions"] = [
Expand All @@ -287,10 +335,14 @@ def save_from_tree(self, tree: AACTree, output_path: str) -> None:
}
]

panel["PanelObjects"].append(button) # type: ignore
panel["PanelObjects"].append(button)

panels["Panels"][page.id] = panel

# Save AssetIndex.plist
with open(resources_dir / "AssetIndex.plist", "wb") as f:
plistlib.dump(assets, f)

# Save panel definitions
with open(resources_dir / "PanelDefinitions.plist", "wb") as f:
plistlib.dump(panels, f)
Expand Down
66 changes: 56 additions & 10 deletions aac_processors/base_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import uuid
import zipfile
from abc import ABC, abstractmethod
from pathlib import Path
from threading import Lock
from typing import Callable, Optional, Union
from typing import Any, Callable, Optional, Union

from .tree_structure import AACTree
from .tree_structure import AACTree, ButtonStyle, ButtonType


class AACProcessor(ABC):
Expand All @@ -19,7 +20,8 @@ def __init__(self) -> None:
"""Initialize the processor."""
self.tree = AACTree()
self._session_id = str(uuid.uuid4())
self._temp_dir: Optional[str] = None
self.source_file: Optional[Path] = None
self.temp_dir: Optional[Path] = None
self._original_filename: Optional[str] = None
self._debug_output: Optional[Callable[[str], None]] = None
self.is_archive = False # Default to non-archive
Expand All @@ -34,17 +36,17 @@ def get_session_workspace(self) -> str:
Raises:
RuntimeError: If workspace creation fails.
"""
if not self._temp_dir:
if not self.temp_dir:
with self._temp_lock:
temp_dir = tempfile.mkdtemp(prefix=f"aac_{self._session_id}_")
if not temp_dir:
raise RuntimeError("Failed to create temporary directory")
self._temp_dir = temp_dir
self.temp_dir = Path(temp_dir)
self._debug_print(f"Created session workspace: {temp_dir}")

if not self._temp_dir: # For type checker
if not self.temp_dir: # For type checker
raise RuntimeError("Temporary directory not available")
return self._temp_dir
return str(self.temp_dir)

def set_source_file(self, file_path: str) -> None:
"""Record the original filename.
Expand All @@ -54,6 +56,7 @@ def set_source_file(self, file_path: str) -> None:
"""
filename = os.path.splitext(os.path.basename(file_path))[0]
self._original_filename = filename
self.source_file = Path(file_path)
self._debug_print(f"Set source file: {filename}")

def _prepare_workspace(self, file_path: str) -> str:
Expand Down Expand Up @@ -124,10 +127,10 @@ def get_output_path(self, target_lang: Optional[str] = None) -> str:

def cleanup_temp_files(self) -> None:
"""Clean up temporary files and directories."""
if self._temp_dir and os.path.exists(self._temp_dir):
if self.temp_dir and os.path.exists(self.temp_dir):
try:
shutil.rmtree(self._temp_dir)
self._temp_dir = None
shutil.rmtree(self.temp_dir)
self.temp_dir = None
self._debug_print("Cleaned up workspace directory")
except Exception as e:
self._debug_print(f"Error cleaning workspace: {str(e)}")
Expand Down Expand Up @@ -259,3 +262,46 @@ def process_texts(
except Exception as e:
self.debug(f"Error processing texts: {str(e)}")
return None


class AACButton:
"""Button in an AAC system."""

def __init__(
self,
id: str,
label: str,
type: ButtonType = ButtonType.SPEAK,
position: tuple[int, int] = (0, 0),
target_page_id: Optional[str] = None,
vocalization: Optional[str] = None,
action: Optional[str] = None,
image: Optional[dict[str, Any]] = None,
width: Optional[float] = None,
height: Optional[float] = None,
) -> None:
"""Initialize button.
Args:
id: Unique identifier
label: Button text
type: Button type (speak, navigate, action)
position: Grid position (row, col)
target_page_id: ID of target page for navigation
vocalization: Text to speak (if different from label)
action: Action to perform
image: Image data dictionary
width: Button width as percentage of page width (0.0-1.0)
height: Button height as percentage of page height (0.0-1.0)
"""
self.id = id
self.label = label
self.type = type
self.position = position
self.target_page_id = target_page_id
self.vocalization = vocalization or label
self.action = action
self.image = image or {}
self.style = ButtonStyle()
self.width = width
self.height = height
Loading

0 comments on commit a58b68b

Please sign in to comment.