Improved snapping. Fixed bug in content embedding.
This commit is contained in:
parent
e972fb864e
commit
eca6d43e6a
@ -665,6 +665,7 @@ class AlignmentManager:
|
|||||||
max_bottom = (page_height - min_gap) - (y + h) # How much we can expand down
|
max_bottom = (page_height - min_gap) - (y + h) # How much we can expand down
|
||||||
|
|
||||||
# Check constraints from other elements
|
# Check constraints from other elements
|
||||||
|
# We need to be conservative and check ALL elements against ALL expansion directions
|
||||||
for other in other_elements:
|
for other in other_elements:
|
||||||
ox, oy = other.position
|
ox, oy = other.position
|
||||||
ow, oh = other.size
|
ow, oh = other.size
|
||||||
@ -681,33 +682,39 @@ class AlignmentManager:
|
|||||||
elem_top = y
|
elem_top = y
|
||||||
elem_bottom = y + h
|
elem_bottom = y + h
|
||||||
|
|
||||||
# Check if elements are aligned horizontally (could affect left/right expansion)
|
# Check leftward expansion
|
||||||
# Two rectangles are "aligned horizontally" if their vertical ranges overlap
|
# An element blocks leftward expansion if:
|
||||||
vertical_overlap = not (elem_bottom < other_top or elem_top > other_bottom)
|
# 1. It's to the left of our left edge (other_right <= elem_left)
|
||||||
|
# 2. Its vertical range would overlap with ANY part of our vertical extent
|
||||||
if vertical_overlap:
|
if other_right <= elem_left:
|
||||||
# Other element is to the left - limits leftward expansion
|
# Check if vertical ranges overlap (current OR after any vertical expansion)
|
||||||
if other_right <= elem_left:
|
# Conservative: assume we might expand vertically to page bounds
|
||||||
|
if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
|
||||||
|
# This element blocks leftward expansion
|
||||||
available_left = elem_left - other_right - min_gap
|
available_left = elem_left - other_right - min_gap
|
||||||
max_left = min(max_left, available_left)
|
max_left = min(max_left, available_left)
|
||||||
|
|
||||||
# Other element is to the right - limits rightward expansion
|
# Check rightward expansion
|
||||||
if other_left >= elem_right:
|
if other_left >= elem_right:
|
||||||
|
# Check if vertical ranges overlap
|
||||||
|
if not (other_bottom <= elem_top - min_gap or other_top >= elem_bottom + min_gap):
|
||||||
|
# This element blocks rightward expansion
|
||||||
available_right = other_left - elem_right - min_gap
|
available_right = other_left - elem_right - min_gap
|
||||||
max_right = min(max_right, available_right)
|
max_right = min(max_right, available_right)
|
||||||
|
|
||||||
# Check if elements are aligned vertically (could affect top/bottom expansion)
|
# Check upward expansion
|
||||||
# Two rectangles are "aligned vertically" if their horizontal ranges overlap
|
if other_bottom <= elem_top:
|
||||||
horizontal_overlap = not (elem_right < other_left or elem_left > other_right)
|
# Check if horizontal ranges overlap
|
||||||
|
if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
|
||||||
if horizontal_overlap:
|
# This element blocks upward expansion
|
||||||
# Other element is above - limits upward expansion
|
|
||||||
if other_bottom <= elem_top:
|
|
||||||
available_top = elem_top - other_bottom - min_gap
|
available_top = elem_top - other_bottom - min_gap
|
||||||
max_top = min(max_top, available_top)
|
max_top = min(max_top, available_top)
|
||||||
|
|
||||||
# Other element is below - limits downward expansion
|
# Check downward expansion
|
||||||
if other_top >= elem_bottom:
|
if other_top >= elem_bottom:
|
||||||
|
# Check if horizontal ranges overlap
|
||||||
|
if not (other_right <= elem_left - min_gap or other_left >= elem_right + min_gap):
|
||||||
|
# This element blocks downward expansion
|
||||||
available_bottom = other_top - elem_bottom - min_gap
|
available_bottom = other_top - elem_bottom - min_gap
|
||||||
max_bottom = min(max_bottom, available_bottom)
|
max_bottom = min(max_bottom, available_bottom)
|
||||||
|
|
||||||
|
|||||||
@ -140,14 +140,25 @@ class AssetHealDialog(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
healed_count = 0
|
healed_count = 0
|
||||||
|
imported_count = 0
|
||||||
still_missing = []
|
still_missing = []
|
||||||
|
|
||||||
# Update asset resolution context with search paths
|
# Update asset resolution context with search paths
|
||||||
set_asset_resolution_context(self.project.folder_path, self.search_paths)
|
set_asset_resolution_context(self.project.folder_path, self.search_paths)
|
||||||
|
|
||||||
# Try to find each missing asset
|
# Build mapping of missing paths to elements
|
||||||
|
path_to_elements: Dict[str, List] = {}
|
||||||
|
for page in self.project.pages:
|
||||||
|
for element in page.layout.elements:
|
||||||
|
if isinstance(element, ImageData) and element.image_path:
|
||||||
|
if element.image_path in self.missing_assets:
|
||||||
|
if element.image_path not in path_to_elements:
|
||||||
|
path_to_elements[element.image_path] = []
|
||||||
|
path_to_elements[element.image_path].append(element)
|
||||||
|
|
||||||
|
# Try to find and import each missing asset
|
||||||
for asset_path in self.missing_assets:
|
for asset_path in self.missing_assets:
|
||||||
found = False
|
found_path = None
|
||||||
filename = os.path.basename(asset_path)
|
filename = os.path.basename(asset_path)
|
||||||
|
|
||||||
# Search in each search path
|
# Search in each search path
|
||||||
@ -155,25 +166,53 @@ class AssetHealDialog(QDialog):
|
|||||||
# Try direct match
|
# Try direct match
|
||||||
candidate = os.path.join(search_path, filename)
|
candidate = os.path.join(search_path, filename)
|
||||||
if os.path.exists(candidate):
|
if os.path.exists(candidate):
|
||||||
found = True
|
found_path = candidate
|
||||||
healed_count += 1
|
|
||||||
print(f"Healed: {asset_path} → {candidate}")
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# Try with same relative path
|
# Try with same relative path
|
||||||
candidate = os.path.join(search_path, asset_path)
|
candidate = os.path.join(search_path, asset_path)
|
||||||
if os.path.exists(candidate):
|
if os.path.exists(candidate):
|
||||||
found = True
|
found_path = candidate
|
||||||
healed_count += 1
|
|
||||||
print(f"Healed: {asset_path} → {candidate}")
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if not found:
|
if found_path:
|
||||||
|
healed_count += 1
|
||||||
|
|
||||||
|
# Check if the found file needs to be imported
|
||||||
|
# (i.e., it's not already in the assets folder)
|
||||||
|
needs_import = True
|
||||||
|
if not os.path.isabs(asset_path) and asset_path.startswith('assets/'):
|
||||||
|
# It's already a relative assets path, just missing from disk
|
||||||
|
# Copy it to the correct location
|
||||||
|
dest_path = os.path.join(self.project.folder_path, asset_path)
|
||||||
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||||
|
import shutil
|
||||||
|
shutil.copy2(found_path, dest_path)
|
||||||
|
print(f"Restored: {asset_path} from {found_path}")
|
||||||
|
else:
|
||||||
|
# It's an absolute path or external path - need to import it
|
||||||
|
try:
|
||||||
|
new_asset_path = self.project.asset_manager.import_asset(found_path)
|
||||||
|
imported_count += 1
|
||||||
|
|
||||||
|
# Update all elements using this path
|
||||||
|
if asset_path in path_to_elements:
|
||||||
|
for element in path_to_elements[asset_path]:
|
||||||
|
element.image_path = new_asset_path
|
||||||
|
|
||||||
|
print(f"Imported and updated: {asset_path} → {new_asset_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing {found_path}: {e}")
|
||||||
|
still_missing.append(asset_path)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
still_missing.append(asset_path)
|
still_missing.append(asset_path)
|
||||||
|
|
||||||
# Report results
|
# Report results
|
||||||
message = f"Healing complete!\n\n"
|
message = f"Healing complete!\n\n"
|
||||||
message += f"Assets healed: {healed_count}\n"
|
message += f"Assets found: {healed_count}\n"
|
||||||
|
if imported_count > 0:
|
||||||
|
message += f"Assets imported to project: {imported_count}\n"
|
||||||
message += f"Still missing: {len(still_missing)}"
|
message += f"Still missing: {len(still_missing)}"
|
||||||
|
|
||||||
if still_missing:
|
if still_missing:
|
||||||
|
|||||||
@ -57,26 +57,34 @@ class AssetDropMixin:
|
|||||||
target_element = self._get_element_at(x, y)
|
target_element = self._get_element_at(x, y)
|
||||||
|
|
||||||
if target_element and isinstance(target_element, (ImageData, PlaceholderData)):
|
if target_element and isinstance(target_element, (ImageData, PlaceholderData)):
|
||||||
if isinstance(target_element, PlaceholderData):
|
main_window = self.window()
|
||||||
new_image = ImageData(
|
if hasattr(main_window, 'project') and main_window.project:
|
||||||
image_path=image_path,
|
try:
|
||||||
x=target_element.position[0],
|
# Import the asset to the project's assets folder
|
||||||
y=target_element.position[1],
|
asset_path = main_window.project.asset_manager.import_asset(image_path)
|
||||||
width=target_element.size[0],
|
|
||||||
height=target_element.size[1],
|
|
||||||
z_index=target_element.z_index
|
|
||||||
)
|
|
||||||
main_window = self.window()
|
|
||||||
if hasattr(main_window, 'project') and main_window.project and main_window.project.pages:
|
|
||||||
for page in main_window.project.pages:
|
|
||||||
if target_element in page.layout.elements:
|
|
||||||
page.layout.elements.remove(target_element)
|
|
||||||
page.layout.add_element(new_image)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
target_element.image_path = image_path
|
|
||||||
|
|
||||||
print(f"Updated element with image: {image_path}")
|
if isinstance(target_element, PlaceholderData):
|
||||||
|
new_image = ImageData(
|
||||||
|
image_path=asset_path, # Use imported asset path
|
||||||
|
x=target_element.position[0],
|
||||||
|
y=target_element.position[1],
|
||||||
|
width=target_element.size[0],
|
||||||
|
height=target_element.size[1],
|
||||||
|
z_index=target_element.z_index
|
||||||
|
)
|
||||||
|
if main_window.project.pages:
|
||||||
|
for page in main_window.project.pages:
|
||||||
|
if target_element in page.layout.elements:
|
||||||
|
page.layout.elements.remove(target_element)
|
||||||
|
page.layout.add_element(new_image)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Update existing ImageData with imported asset
|
||||||
|
target_element.image_path = asset_path
|
||||||
|
|
||||||
|
print(f"Updated element with image: {asset_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error importing dropped image: {e}")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|||||||
@ -68,7 +68,8 @@ class ElementManipulationMixin:
|
|||||||
dy=dy,
|
dy=dy,
|
||||||
resize_handle=self.resize_handle,
|
resize_handle=self.resize_handle,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
dpi=dpi
|
dpi=dpi,
|
||||||
|
project=main_window.project
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply the snapped values
|
# Apply the snapped values
|
||||||
|
|||||||
@ -197,7 +197,8 @@ class MouseInteractionMixin:
|
|||||||
position=(new_x, new_y),
|
position=(new_x, new_y),
|
||||||
size=self.selected_element.size,
|
size=self.selected_element.size,
|
||||||
page_size=page_size,
|
page_size=page_size,
|
||||||
dpi=dpi
|
dpi=dpi,
|
||||||
|
project=main_window.project
|
||||||
)
|
)
|
||||||
|
|
||||||
self.selected_element.position = snapped_pos
|
self.selected_element.position = snapped_pos
|
||||||
|
|||||||
@ -80,14 +80,12 @@ class ViewOperationsMixin:
|
|||||||
)
|
)
|
||||||
def toggle_grid_snap(self):
|
def toggle_grid_snap(self):
|
||||||
"""Toggle grid snapping"""
|
"""Toggle grid snapping"""
|
||||||
current_page = self.get_current_page()
|
if not self.project:
|
||||||
if not current_page:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
snap_sys = current_page.layout.snapping_system
|
self.project.snap_to_grid = not self.project.snap_to_grid
|
||||||
snap_sys.snap_to_grid = not snap_sys.snap_to_grid
|
|
||||||
|
status = "enabled" if self.project.snap_to_grid else "disabled"
|
||||||
status = "enabled" if snap_sys.snap_to_grid else "disabled"
|
|
||||||
self.update_view()
|
self.update_view()
|
||||||
self.show_status(f"Grid snapping {status}", 2000)
|
self.show_status(f"Grid snapping {status}", 2000)
|
||||||
print(f"Grid snapping {status}")
|
print(f"Grid snapping {status}")
|
||||||
@ -100,14 +98,12 @@ class ViewOperationsMixin:
|
|||||||
)
|
)
|
||||||
def toggle_edge_snap(self):
|
def toggle_edge_snap(self):
|
||||||
"""Toggle edge snapping"""
|
"""Toggle edge snapping"""
|
||||||
current_page = self.get_current_page()
|
if not self.project:
|
||||||
if not current_page:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
snap_sys = current_page.layout.snapping_system
|
self.project.snap_to_edges = not self.project.snap_to_edges
|
||||||
snap_sys.snap_to_edges = not snap_sys.snap_to_edges
|
|
||||||
|
status = "enabled" if self.project.snap_to_edges else "disabled"
|
||||||
status = "enabled" if snap_sys.snap_to_edges else "disabled"
|
|
||||||
self.update_view()
|
self.update_view()
|
||||||
self.show_status(f"Edge snapping {status}", 2000)
|
self.show_status(f"Edge snapping {status}", 2000)
|
||||||
print(f"Edge snapping {status}")
|
print(f"Edge snapping {status}")
|
||||||
@ -120,36 +116,51 @@ class ViewOperationsMixin:
|
|||||||
)
|
)
|
||||||
def toggle_guide_snap(self):
|
def toggle_guide_snap(self):
|
||||||
"""Toggle guide snapping"""
|
"""Toggle guide snapping"""
|
||||||
current_page = self.get_current_page()
|
if not self.project:
|
||||||
if not current_page:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
snap_sys = current_page.layout.snapping_system
|
self.project.snap_to_guides = not self.project.snap_to_guides
|
||||||
snap_sys.snap_to_guides = not snap_sys.snap_to_guides
|
|
||||||
|
status = "enabled" if self.project.snap_to_guides else "disabled"
|
||||||
status = "enabled" if snap_sys.snap_to_guides else "disabled"
|
|
||||||
self.update_view()
|
self.update_view()
|
||||||
self.show_status(f"Guide snapping {status}", 2000)
|
self.show_status(f"Guide snapping {status}", 2000)
|
||||||
print(f"Guide snapping {status}")
|
print(f"Guide snapping {status}")
|
||||||
|
|
||||||
@ribbon_action(
|
@ribbon_action(
|
||||||
label="Toggle Snap Lines",
|
label="Show Grid",
|
||||||
tooltip="Toggle visibility of snap lines",
|
tooltip="Toggle visibility of grid lines",
|
||||||
|
tab="View",
|
||||||
|
group="Snapping"
|
||||||
|
)
|
||||||
|
def toggle_show_grid(self):
|
||||||
|
"""Toggle grid visibility"""
|
||||||
|
if not self.project:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.project.show_grid = not self.project.show_grid
|
||||||
|
|
||||||
|
status = "visible" if self.project.show_grid else "hidden"
|
||||||
|
self.update_view()
|
||||||
|
self.show_status(f"Grid {status}", 2000)
|
||||||
|
print(f"Grid {status}")
|
||||||
|
|
||||||
|
@ribbon_action(
|
||||||
|
label="Show Guides",
|
||||||
|
tooltip="Toggle visibility of guide lines",
|
||||||
tab="View",
|
tab="View",
|
||||||
group="Snapping"
|
group="Snapping"
|
||||||
)
|
)
|
||||||
def toggle_snap_lines(self):
|
def toggle_snap_lines(self):
|
||||||
"""Toggle snap lines visibility"""
|
"""Toggle guide lines visibility"""
|
||||||
current_page = self.get_current_page()
|
if not self.project:
|
||||||
if not current_page:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
current_page.layout.show_snap_lines = not current_page.layout.show_snap_lines
|
self.project.show_snap_lines = not self.project.show_snap_lines
|
||||||
|
|
||||||
status = "visible" if current_page.layout.show_snap_lines else "hidden"
|
status = "visible" if self.project.show_snap_lines else "hidden"
|
||||||
self.update_view()
|
self.update_view()
|
||||||
self.show_status(f"Snap lines {status}", 2000)
|
self.show_status(f"Guides {status}", 2000)
|
||||||
print(f"Snap lines {status}")
|
print(f"Guides {status}")
|
||||||
|
|
||||||
@ribbon_action(
|
@ribbon_action(
|
||||||
label="Add H Guide",
|
label="Add H Guide",
|
||||||
@ -219,48 +230,45 @@ class ViewOperationsMixin:
|
|||||||
def set_grid_size(self):
|
def set_grid_size(self):
|
||||||
"""Open dialog to set grid size"""
|
"""Open dialog to set grid size"""
|
||||||
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QPushButton
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QDoubleSpinBox, QPushButton
|
||||||
|
|
||||||
current_page = self.get_current_page()
|
if not self.project:
|
||||||
if not current_page:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
snap_sys = current_page.layout.snapping_system
|
|
||||||
|
|
||||||
# Create dialog
|
# Create dialog
|
||||||
dialog = QDialog(self)
|
dialog = QDialog(self)
|
||||||
dialog.setWindowTitle("Grid Settings")
|
dialog.setWindowTitle("Grid Settings")
|
||||||
dialog.setMinimumWidth(300)
|
dialog.setMinimumWidth(300)
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Grid size setting
|
# Grid size setting
|
||||||
size_layout = QHBoxLayout()
|
size_layout = QHBoxLayout()
|
||||||
size_layout.addWidget(QLabel("Grid Size:"))
|
size_layout.addWidget(QLabel("Grid Size:"))
|
||||||
|
|
||||||
size_spinbox = QDoubleSpinBox()
|
size_spinbox = QDoubleSpinBox()
|
||||||
size_spinbox.setRange(1.0, 100.0)
|
size_spinbox.setRange(1.0, 100.0)
|
||||||
size_spinbox.setValue(snap_sys.grid_size_mm)
|
size_spinbox.setValue(self.project.grid_size_mm)
|
||||||
size_spinbox.setSuffix(" mm")
|
size_spinbox.setSuffix(" mm")
|
||||||
size_spinbox.setDecimals(1)
|
size_spinbox.setDecimals(1)
|
||||||
size_spinbox.setSingleStep(1.0)
|
size_spinbox.setSingleStep(1.0)
|
||||||
size_layout.addWidget(size_spinbox)
|
size_layout.addWidget(size_spinbox)
|
||||||
|
|
||||||
layout.addLayout(size_layout)
|
layout.addLayout(size_layout)
|
||||||
|
|
||||||
# Snap threshold setting
|
# Snap threshold setting
|
||||||
threshold_layout = QHBoxLayout()
|
threshold_layout = QHBoxLayout()
|
||||||
threshold_layout.addWidget(QLabel("Snap Threshold:"))
|
threshold_layout.addWidget(QLabel("Snap Threshold:"))
|
||||||
|
|
||||||
threshold_spinbox = QDoubleSpinBox()
|
threshold_spinbox = QDoubleSpinBox()
|
||||||
threshold_spinbox.setRange(0.5, 20.0)
|
threshold_spinbox.setRange(0.5, 20.0)
|
||||||
threshold_spinbox.setValue(snap_sys.snap_threshold_mm)
|
threshold_spinbox.setValue(self.project.snap_threshold_mm)
|
||||||
threshold_spinbox.setSuffix(" mm")
|
threshold_spinbox.setSuffix(" mm")
|
||||||
threshold_spinbox.setDecimals(1)
|
threshold_spinbox.setDecimals(1)
|
||||||
threshold_spinbox.setSingleStep(0.5)
|
threshold_spinbox.setSingleStep(0.5)
|
||||||
threshold_layout.addWidget(threshold_spinbox)
|
threshold_layout.addWidget(threshold_spinbox)
|
||||||
|
|
||||||
layout.addLayout(threshold_layout)
|
layout.addLayout(threshold_layout)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
cancel_btn = QPushButton("Cancel")
|
cancel_btn = QPushButton("Cancel")
|
||||||
@ -268,22 +276,22 @@ class ViewOperationsMixin:
|
|||||||
ok_btn = QPushButton("OK")
|
ok_btn = QPushButton("OK")
|
||||||
ok_btn.clicked.connect(dialog.accept)
|
ok_btn.clicked.connect(dialog.accept)
|
||||||
ok_btn.setDefault(True)
|
ok_btn.setDefault(True)
|
||||||
|
|
||||||
button_layout.addStretch()
|
button_layout.addStretch()
|
||||||
button_layout.addWidget(cancel_btn)
|
button_layout.addWidget(cancel_btn)
|
||||||
button_layout.addWidget(ok_btn)
|
button_layout.addWidget(ok_btn)
|
||||||
layout.addLayout(button_layout)
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
dialog.setLayout(layout)
|
dialog.setLayout(layout)
|
||||||
|
|
||||||
# Show dialog and apply if accepted
|
# Show dialog and apply if accepted
|
||||||
if dialog.exec() == QDialog.DialogCode.Accepted:
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
new_grid_size = size_spinbox.value()
|
new_grid_size = size_spinbox.value()
|
||||||
new_threshold = threshold_spinbox.value()
|
new_threshold = threshold_spinbox.value()
|
||||||
|
|
||||||
snap_sys.grid_size_mm = new_grid_size
|
self.project.grid_size_mm = new_grid_size
|
||||||
snap_sys.snap_threshold_mm = new_threshold
|
self.project.snap_threshold_mm = new_threshold
|
||||||
|
|
||||||
self.update_view()
|
self.update_view()
|
||||||
self.show_status(f"Grid size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm", 2000)
|
self.show_status(f"Grid size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm", 2000)
|
||||||
print(f"Updated grid settings - Size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm")
|
print(f"Updated grid settings - Size: {new_grid_size:.1f}mm, Threshold: {new_threshold:.1f}mm")
|
||||||
@ -325,12 +333,22 @@ class ViewOperationsMixin:
|
|||||||
|
|
||||||
@ribbon_action(
|
@ribbon_action(
|
||||||
label="Show Grid",
|
label="Show Grid",
|
||||||
tooltip="Toggle visibility of snap lines",
|
tooltip="Toggle visibility of grid lines",
|
||||||
|
tab="Layout",
|
||||||
|
group="Snapping"
|
||||||
|
)
|
||||||
|
def layout_toggle_show_grid(self):
|
||||||
|
"""Toggle grid visibility (Layout tab)"""
|
||||||
|
self.toggle_show_grid()
|
||||||
|
|
||||||
|
@ribbon_action(
|
||||||
|
label="Show Guides",
|
||||||
|
tooltip="Toggle visibility of guide lines",
|
||||||
tab="Layout",
|
tab="Layout",
|
||||||
group="Snapping"
|
group="Snapping"
|
||||||
)
|
)
|
||||||
def layout_toggle_snap_lines(self):
|
def layout_toggle_snap_lines(self):
|
||||||
"""Toggle snap lines visibility (Layout tab)"""
|
"""Toggle guide lines visibility (Layout tab)"""
|
||||||
self.toggle_snap_lines()
|
self.toggle_snap_lines()
|
||||||
|
|
||||||
@ribbon_action(
|
@ribbon_action(
|
||||||
|
|||||||
@ -69,7 +69,7 @@ class RenderingMixin:
|
|||||||
renderer.begin_render()
|
renderer.begin_render()
|
||||||
# Pass widget reference for async loading
|
# Pass widget reference for async loading
|
||||||
page.layout._parent_widget = self
|
page.layout._parent_widget = self
|
||||||
page.layout.render(dpi=dpi)
|
page.layout.render(dpi=dpi, project=main_window.project)
|
||||||
renderer.end_render()
|
renderer.end_render()
|
||||||
|
|
||||||
elif page_type == 'ghost':
|
elif page_type == 'ghost':
|
||||||
|
|||||||
@ -39,15 +39,16 @@ class PageLayout:
|
|||||||
"""Set a grid layout for the page"""
|
"""Set a grid layout for the page"""
|
||||||
self.grid_layout = grid
|
self.grid_layout = grid
|
||||||
|
|
||||||
def render(self, dpi: int = 300):
|
def render(self, dpi: int = 300, project=None):
|
||||||
"""
|
"""
|
||||||
Render all elements on the page in page-local coordinates.
|
Render all elements on the page in page-local coordinates.
|
||||||
|
|
||||||
Note: This method assumes OpenGL transformations have already been set up
|
Note: This method assumes OpenGL transformations have already been set up
|
||||||
by PageRenderer.begin_render(). All coordinates here are in page-local space.
|
by PageRenderer.begin_render(). All coordinates here are in page-local space.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dpi: Working DPI for converting mm to pixels
|
dpi: Working DPI for converting mm to pixels
|
||||||
|
project: Optional project instance for global snapping settings
|
||||||
"""
|
"""
|
||||||
from OpenGL.GL import glBegin, glEnd, glVertex2f, glColor3f, GL_QUADS, GL_LINE_LOOP, GL_LINES, glLineWidth, glDisable, glEnable, GL_DEPTH_TEST
|
from OpenGL.GL import glBegin, glEnd, glVertex2f, glColor3f, GL_QUADS, GL_LINE_LOOP, GL_LINES, glLineWidth, glDisable, glEnable, GL_DEPTH_TEST
|
||||||
|
|
||||||
@ -134,26 +135,55 @@ class PageLayout:
|
|||||||
glEnd()
|
glEnd()
|
||||||
glLineWidth(1.0)
|
glLineWidth(1.0)
|
||||||
|
|
||||||
# Always render snap lines (grid shows when snap_to_grid is on, guides show when show_snap_lines is on)
|
# Always render snap lines (grid shows when show_grid is on, guides show when show_snap_lines is on)
|
||||||
self._render_snap_lines(dpi, page_x, page_y)
|
self._render_snap_lines(dpi, page_x, page_y, project)
|
||||||
|
|
||||||
# Re-enable depth testing
|
# Re-enable depth testing
|
||||||
glEnable(GL_DEPTH_TEST)
|
glEnable(GL_DEPTH_TEST)
|
||||||
|
|
||||||
def _render_snap_lines(self, dpi: int, page_x: float, page_y: float):
|
def _render_snap_lines(self, dpi: int, page_x: float, page_y: float, project=None):
|
||||||
"""Render snap lines (grid, edges, guides)"""
|
"""Render snap lines (grid, edges, guides)"""
|
||||||
from OpenGL.GL import (glColor3f, glColor4f, glLineWidth, glBegin, glEnd,
|
from OpenGL.GL import (glColor3f, glColor4f, glLineWidth, glBegin, glEnd,
|
||||||
glVertex2f, GL_LINES, glEnable, glDisable, GL_BLEND,
|
glVertex2f, GL_LINES, glEnable, glDisable, GL_BLEND,
|
||||||
glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
glBlendFunc, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||||
|
|
||||||
snap_lines = self.snapping_system.get_snap_lines(self.size, dpi)
|
# Use project settings if available, otherwise fall back to local snapping_system
|
||||||
|
if project:
|
||||||
|
# Use project-level global settings
|
||||||
|
snap_to_grid = project.snap_to_grid
|
||||||
|
snap_to_edges = project.snap_to_edges
|
||||||
|
snap_to_guides = project.snap_to_guides
|
||||||
|
grid_size_mm = project.grid_size_mm
|
||||||
|
snap_threshold_mm = project.snap_threshold_mm
|
||||||
|
show_grid = project.show_grid
|
||||||
|
show_snap_lines = project.show_snap_lines
|
||||||
|
else:
|
||||||
|
# Fall back to per-page settings (backward compatibility)
|
||||||
|
snap_to_grid = self.snapping_system.snap_to_grid
|
||||||
|
snap_to_edges = self.snapping_system.snap_to_edges
|
||||||
|
snap_to_guides = self.snapping_system.snap_to_guides
|
||||||
|
grid_size_mm = self.snapping_system.grid_size_mm
|
||||||
|
snap_threshold_mm = self.snapping_system.snap_threshold_mm
|
||||||
|
show_grid = snap_to_grid # Old behavior: grid only shows when snapping
|
||||||
|
show_snap_lines = self.show_snap_lines
|
||||||
|
|
||||||
|
# Create a temporary snapping system with project settings to get snap lines
|
||||||
|
from pyPhotoAlbum.snapping import SnappingSystem
|
||||||
|
temp_snap_sys = SnappingSystem(snap_threshold_mm=snap_threshold_mm)
|
||||||
|
temp_snap_sys.grid_size_mm = grid_size_mm
|
||||||
|
temp_snap_sys.snap_to_grid = snap_to_grid
|
||||||
|
temp_snap_sys.snap_to_edges = snap_to_edges
|
||||||
|
temp_snap_sys.snap_to_guides = snap_to_guides
|
||||||
|
temp_snap_sys.guides = self.snapping_system.guides # Use page-specific guides
|
||||||
|
|
||||||
|
snap_lines = temp_snap_sys.get_snap_lines(self.size, dpi)
|
||||||
|
|
||||||
# Enable alpha blending for transparency
|
# Enable alpha blending for transparency
|
||||||
glEnable(GL_BLEND)
|
glEnable(GL_BLEND)
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||||
|
|
||||||
# Draw grid lines (darker gray with transparency) - always visible when snap_to_grid is enabled
|
# Draw grid lines (darker gray with transparency) - visible when show_grid is enabled
|
||||||
if self.snapping_system.snap_to_grid and snap_lines['grid']:
|
if show_grid and snap_lines['grid']:
|
||||||
glColor4f(0.6, 0.6, 0.6, 0.4) # Gray with 40% opacity
|
glColor4f(0.6, 0.6, 0.6, 0.4) # Gray with 40% opacity
|
||||||
glLineWidth(1.0)
|
glLineWidth(1.0)
|
||||||
for orientation, position in snap_lines['grid']:
|
for orientation, position in snap_lines['grid']:
|
||||||
@ -165,9 +195,9 @@ class PageLayout:
|
|||||||
glVertex2f(page_x, page_y + position)
|
glVertex2f(page_x, page_y + position)
|
||||||
glVertex2f(page_x + self.size[0] * dpi / 25.4, page_y + position)
|
glVertex2f(page_x + self.size[0] * dpi / 25.4, page_y + position)
|
||||||
glEnd()
|
glEnd()
|
||||||
|
|
||||||
# Draw guides (cyan, more visible with transparency) - only show when show_snap_lines is on
|
# Draw guides (cyan, more visible with transparency) - only show when show_snap_lines is on
|
||||||
if self.show_snap_lines and snap_lines['guides']:
|
if show_snap_lines and snap_lines['guides']:
|
||||||
glColor4f(0.0, 0.7, 0.9, 0.8) # Cyan with 80% opacity
|
glColor4f(0.0, 0.7, 0.9, 0.8) # Cyan with 80% opacity
|
||||||
glLineWidth(1.5)
|
glLineWidth(1.5)
|
||||||
for orientation, position in snap_lines['guides']:
|
for orientation, position in snap_lines['guides']:
|
||||||
@ -179,7 +209,7 @@ class PageLayout:
|
|||||||
glVertex2f(page_x, page_y + position)
|
glVertex2f(page_x, page_y + position)
|
||||||
glVertex2f(page_x + self.size[0] * dpi / 25.4, page_y + position)
|
glVertex2f(page_x + self.size[0] * dpi / 25.4, page_y + position)
|
||||||
glEnd()
|
glEnd()
|
||||||
|
|
||||||
glLineWidth(1.0)
|
glLineWidth(1.0)
|
||||||
glDisable(GL_BLEND)
|
glDisable(GL_BLEND)
|
||||||
|
|
||||||
|
|||||||
@ -123,6 +123,15 @@ class Project:
|
|||||||
# Using TemporaryDirectory instance that auto-cleans on deletion
|
# Using TemporaryDirectory instance that auto-cleans on deletion
|
||||||
self._temp_dir = None
|
self._temp_dir = None
|
||||||
|
|
||||||
|
# Global snapping settings (apply to all pages)
|
||||||
|
self.snap_to_grid = False
|
||||||
|
self.snap_to_edges = True
|
||||||
|
self.snap_to_guides = True
|
||||||
|
self.grid_size_mm = 10.0
|
||||||
|
self.snap_threshold_mm = 5.0
|
||||||
|
self.show_grid = False # Show grid lines independently of snap_to_grid
|
||||||
|
self.show_snap_lines = True # Show snap lines (guides) during dragging
|
||||||
|
|
||||||
# Initialize asset manager
|
# Initialize asset manager
|
||||||
self.asset_manager = AssetManager(self.folder_path)
|
self.asset_manager = AssetManager(self.folder_path)
|
||||||
|
|
||||||
@ -317,6 +326,13 @@ class Project:
|
|||||||
"cover_bleed_mm": self.cover_bleed_mm,
|
"cover_bleed_mm": self.cover_bleed_mm,
|
||||||
"binding_type": self.binding_type,
|
"binding_type": self.binding_type,
|
||||||
"embedded_templates": self.embedded_templates,
|
"embedded_templates": self.embedded_templates,
|
||||||
|
"snap_to_grid": self.snap_to_grid,
|
||||||
|
"snap_to_edges": self.snap_to_edges,
|
||||||
|
"snap_to_guides": self.snap_to_guides,
|
||||||
|
"grid_size_mm": self.grid_size_mm,
|
||||||
|
"snap_threshold_mm": self.snap_threshold_mm,
|
||||||
|
"show_grid": self.show_grid,
|
||||||
|
"show_snap_lines": self.show_snap_lines,
|
||||||
"pages": [page.serialize() for page in self.pages],
|
"pages": [page.serialize() for page in self.pages],
|
||||||
"history": self.history.serialize(),
|
"history": self.history.serialize(),
|
||||||
"asset_manager": self.asset_manager.serialize()
|
"asset_manager": self.asset_manager.serialize()
|
||||||
@ -337,9 +353,18 @@ class Project:
|
|||||||
self.paper_thickness_mm = data.get("paper_thickness_mm", 0.2)
|
self.paper_thickness_mm = data.get("paper_thickness_mm", 0.2)
|
||||||
self.cover_bleed_mm = data.get("cover_bleed_mm", 0.0)
|
self.cover_bleed_mm = data.get("cover_bleed_mm", 0.0)
|
||||||
self.binding_type = data.get("binding_type", "saddle_stitch")
|
self.binding_type = data.get("binding_type", "saddle_stitch")
|
||||||
|
|
||||||
# Deserialize embedded templates
|
# Deserialize embedded templates
|
||||||
self.embedded_templates = data.get("embedded_templates", {})
|
self.embedded_templates = data.get("embedded_templates", {})
|
||||||
|
|
||||||
|
# Deserialize global snapping settings
|
||||||
|
self.snap_to_grid = data.get("snap_to_grid", False)
|
||||||
|
self.snap_to_edges = data.get("snap_to_edges", True)
|
||||||
|
self.snap_to_guides = data.get("snap_to_guides", True)
|
||||||
|
self.grid_size_mm = data.get("grid_size_mm", 10.0)
|
||||||
|
self.snap_threshold_mm = data.get("snap_threshold_mm", 5.0)
|
||||||
|
self.show_grid = data.get("show_grid", False)
|
||||||
|
self.show_snap_lines = data.get("show_snap_lines", True)
|
||||||
|
|
||||||
self.pages = []
|
self.pages = []
|
||||||
|
|
||||||
|
|||||||
@ -22,6 +22,56 @@ from pyPhotoAlbum.version_manager import (
|
|||||||
SERIALIZATION_VERSION = CURRENT_DATA_VERSION
|
SERIALIZATION_VERSION = CURRENT_DATA_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
def _import_external_images(project: Project):
|
||||||
|
"""
|
||||||
|
Find and import any images that have external (absolute or non-assets) paths.
|
||||||
|
This ensures all images are in the assets folder before saving.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project: The Project instance to check
|
||||||
|
"""
|
||||||
|
from pyPhotoAlbum.models import ImageData
|
||||||
|
|
||||||
|
imported_count = 0
|
||||||
|
|
||||||
|
for page in project.pages:
|
||||||
|
for element in page.layout.elements:
|
||||||
|
if isinstance(element, ImageData) and element.image_path:
|
||||||
|
# Check if this is an external path (absolute or not in assets/)
|
||||||
|
is_external = False
|
||||||
|
|
||||||
|
if os.path.isabs(element.image_path):
|
||||||
|
# Absolute path - definitely external
|
||||||
|
is_external = True
|
||||||
|
external_path = element.image_path
|
||||||
|
elif not element.image_path.startswith('assets/'):
|
||||||
|
# Relative path but not in assets folder
|
||||||
|
# Check if it exists relative to project folder
|
||||||
|
full_path = os.path.join(project.folder_path, element.image_path)
|
||||||
|
if os.path.exists(full_path) and not full_path.startswith(project.asset_manager.assets_folder):
|
||||||
|
is_external = True
|
||||||
|
external_path = full_path
|
||||||
|
else:
|
||||||
|
# Path doesn't exist - skip it (will be caught as missing asset)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
# Already in assets/ folder
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Import the external image
|
||||||
|
if is_external and os.path.exists(external_path):
|
||||||
|
try:
|
||||||
|
new_asset_path = project.asset_manager.import_asset(external_path)
|
||||||
|
element.image_path = new_asset_path
|
||||||
|
imported_count += 1
|
||||||
|
print(f"Auto-imported external image: {external_path} → {new_asset_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: Failed to import external image {external_path}: {e}")
|
||||||
|
|
||||||
|
if imported_count > 0:
|
||||||
|
print(f"Auto-imported {imported_count} external image(s) to assets folder")
|
||||||
|
|
||||||
|
|
||||||
def _normalize_asset_paths(project: Project, project_folder: str):
|
def _normalize_asset_paths(project: Project, project_folder: str):
|
||||||
"""
|
"""
|
||||||
Normalize asset paths in a loaded project to be relative to the project folder.
|
Normalize asset paths in a loaded project to be relative to the project folder.
|
||||||
@ -73,11 +123,11 @@ def _normalize_asset_paths(project: Project, project_folder: str):
|
|||||||
def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]:
|
def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]:
|
||||||
"""
|
"""
|
||||||
Save a project to a ZIP file, including all assets.
|
Save a project to a ZIP file, including all assets.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project: The Project instance to save
|
project: The Project instance to save
|
||||||
zip_path: Path where the ZIP file should be created
|
zip_path: Path where the ZIP file should be created
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (success: bool, error_message: Optional[str])
|
Tuple of (success: bool, error_message: Optional[str])
|
||||||
"""
|
"""
|
||||||
@ -85,7 +135,10 @@ def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]:
|
|||||||
# Ensure .ppz extension
|
# Ensure .ppz extension
|
||||||
if not zip_path.lower().endswith('.ppz'):
|
if not zip_path.lower().endswith('.ppz'):
|
||||||
zip_path += '.ppz'
|
zip_path += '.ppz'
|
||||||
|
|
||||||
|
# Check for and import any external images before saving
|
||||||
|
_import_external_images(project)
|
||||||
|
|
||||||
# Serialize project to dictionary
|
# Serialize project to dictionary
|
||||||
project_data = project.serialize()
|
project_data = project.serialize()
|
||||||
|
|
||||||
@ -98,7 +151,7 @@ def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]:
|
|||||||
# Write project.json
|
# Write project.json
|
||||||
project_json = json.dumps(project_data, indent=2)
|
project_json = json.dumps(project_data, indent=2)
|
||||||
zipf.writestr('project.json', project_json)
|
zipf.writestr('project.json', project_json)
|
||||||
|
|
||||||
# Add all files from the assets folder
|
# Add all files from the assets folder
|
||||||
assets_folder = project.asset_manager.assets_folder
|
assets_folder = project.asset_manager.assets_folder
|
||||||
if os.path.exists(assets_folder):
|
if os.path.exists(assets_folder):
|
||||||
@ -108,10 +161,10 @@ def save_to_zip(project: Project, zip_path: str) -> Tuple[bool, Optional[str]]:
|
|||||||
# Store with relative path from project folder
|
# Store with relative path from project folder
|
||||||
arcname = os.path.relpath(file_path, project.folder_path)
|
arcname = os.path.relpath(file_path, project.folder_path)
|
||||||
zipf.write(file_path, arcname)
|
zipf.write(file_path, arcname)
|
||||||
|
|
||||||
print(f"Project saved to {zip_path}")
|
print(f"Project saved to {zip_path}")
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error saving project: {str(e)}"
|
error_msg = f"Error saving project: {str(e)}"
|
||||||
print(error_msg)
|
print(error_msg)
|
||||||
|
|||||||
@ -61,40 +61,56 @@ class SnappingSystem:
|
|||||||
"""Remove all guides"""
|
"""Remove all guides"""
|
||||||
self.guides.clear()
|
self.guides.clear()
|
||||||
|
|
||||||
def snap_position(self,
|
def snap_position(self,
|
||||||
position: Tuple[float, float],
|
position: Tuple[float, float],
|
||||||
size: Tuple[float, float],
|
size: Tuple[float, float],
|
||||||
page_size: Tuple[float, float],
|
page_size: Tuple[float, float],
|
||||||
dpi: int = 300) -> Tuple[float, float]:
|
dpi: int = 300,
|
||||||
|
project=None) -> Tuple[float, float]:
|
||||||
"""
|
"""
|
||||||
Apply snapping to a position using combined distance threshold
|
Apply snapping to a position using combined distance threshold
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position: Current position (x, y) in pixels
|
position: Current position (x, y) in pixels
|
||||||
size: Element size (width, height) in pixels
|
size: Element size (width, height) in pixels
|
||||||
page_size: Page size (width, height) in mm
|
page_size: Page size (width, height) in mm
|
||||||
dpi: DPI for conversion
|
dpi: DPI for conversion
|
||||||
|
project: Optional project for global snapping settings
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Snapped position (x, y) in pixels
|
Snapped position (x, y) in pixels
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
|
||||||
x, y = position
|
x, y = position
|
||||||
width, height = size
|
width, height = size
|
||||||
page_width_mm, page_height_mm = page_size
|
page_width_mm, page_height_mm = page_size
|
||||||
|
|
||||||
|
# Use project settings if available, otherwise use local settings
|
||||||
|
if project:
|
||||||
|
snap_to_grid = project.snap_to_grid
|
||||||
|
snap_to_edges = project.snap_to_edges
|
||||||
|
snap_to_guides = project.snap_to_guides
|
||||||
|
grid_size_mm = project.grid_size_mm
|
||||||
|
snap_threshold_mm = project.snap_threshold_mm
|
||||||
|
else:
|
||||||
|
snap_to_grid = self.snap_to_grid
|
||||||
|
snap_to_edges = self.snap_to_edges
|
||||||
|
snap_to_guides = self.snap_to_guides
|
||||||
|
grid_size_mm = self.grid_size_mm
|
||||||
|
snap_threshold_mm = self.snap_threshold_mm
|
||||||
|
|
||||||
# Convert threshold from mm to pixels
|
# Convert threshold from mm to pixels
|
||||||
snap_threshold_px = self.snap_threshold_mm * dpi / 25.4
|
snap_threshold_px = snap_threshold_mm * dpi / 25.4
|
||||||
|
|
||||||
# Collect all potential snap points for both edges of the element
|
# Collect all potential snap points for both edges of the element
|
||||||
snap_points = []
|
snap_points = []
|
||||||
|
|
||||||
# 1. Page edge snap points
|
# 1. Page edge snap points
|
||||||
if self.snap_to_edges:
|
if snap_to_edges:
|
||||||
page_width_px = page_width_mm * dpi / 25.4
|
page_width_px = page_width_mm * dpi / 25.4
|
||||||
page_height_px = page_height_mm * dpi / 25.4
|
page_height_px = page_height_mm * dpi / 25.4
|
||||||
|
|
||||||
# Corners where element's top-left can snap
|
# Corners where element's top-left can snap
|
||||||
snap_points.extend([
|
snap_points.extend([
|
||||||
(0, 0), # Top-left corner
|
(0, 0), # Top-left corner
|
||||||
@ -102,7 +118,7 @@ class SnappingSystem:
|
|||||||
(0, page_height_px - height), # Bottom-left corner
|
(0, page_height_px - height), # Bottom-left corner
|
||||||
(page_width_px - width, page_height_px - height), # Bottom-right corner
|
(page_width_px - width, page_height_px - height), # Bottom-right corner
|
||||||
])
|
])
|
||||||
|
|
||||||
# Edge positions (element aligned to edge on one axis)
|
# Edge positions (element aligned to edge on one axis)
|
||||||
snap_points.extend([
|
snap_points.extend([
|
||||||
(0, y), # Left edge
|
(0, y), # Left edge
|
||||||
@ -110,10 +126,10 @@ class SnappingSystem:
|
|||||||
(x, 0), # Top edge
|
(x, 0), # Top edge
|
||||||
(x, page_height_px - height), # Bottom edge
|
(x, page_height_px - height), # Bottom edge
|
||||||
])
|
])
|
||||||
|
|
||||||
# 2. Grid snap points
|
# 2. Grid snap points
|
||||||
if self.snap_to_grid:
|
if snap_to_grid:
|
||||||
grid_size_px = self.grid_size_mm * dpi / 25.4
|
grid_size_px = grid_size_mm * dpi / 25.4
|
||||||
page_width_px = page_width_mm * dpi / 25.4
|
page_width_px = page_width_mm * dpi / 25.4
|
||||||
page_height_px = page_height_mm * dpi / 25.4
|
page_height_px = page_height_mm * dpi / 25.4
|
||||||
|
|
||||||
@ -137,7 +153,7 @@ class SnappingSystem:
|
|||||||
grid_x += grid_size_px
|
grid_x += grid_size_px
|
||||||
|
|
||||||
# 3. Guide snap points
|
# 3. Guide snap points
|
||||||
if self.snap_to_guides:
|
if snap_to_guides:
|
||||||
vertical_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == 'vertical']
|
vertical_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == 'vertical']
|
||||||
horizontal_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == 'horizontal']
|
horizontal_guides = [g.position * dpi / 25.4 for g in self.guides if g.orientation == 'horizontal']
|
||||||
|
|
||||||
@ -173,10 +189,11 @@ class SnappingSystem:
|
|||||||
dy: float,
|
dy: float,
|
||||||
resize_handle: str,
|
resize_handle: str,
|
||||||
page_size: Tuple[float, float],
|
page_size: Tuple[float, float],
|
||||||
dpi: int = 300) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
dpi: int = 300,
|
||||||
|
project=None) -> Tuple[Tuple[float, float], Tuple[float, float]]:
|
||||||
"""
|
"""
|
||||||
Apply snapping during resize operations
|
Apply snapping during resize operations
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
position: Current position (x, y) in pixels
|
position: Current position (x, y) in pixels
|
||||||
size: Current size (width, height) in pixels
|
size: Current size (width, height) in pixels
|
||||||
@ -185,16 +202,23 @@ class SnappingSystem:
|
|||||||
resize_handle: Which handle is being dragged ('nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w')
|
resize_handle: Which handle is being dragged ('nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w')
|
||||||
page_size: Page size (width, height) in mm
|
page_size: Page size (width, height) in mm
|
||||||
dpi: DPI for conversion
|
dpi: DPI for conversion
|
||||||
|
project: Optional project for global snapping settings
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (snapped_position, snapped_size) in pixels
|
Tuple of (snapped_position, snapped_size) in pixels
|
||||||
"""
|
"""
|
||||||
x, y = position
|
x, y = position
|
||||||
width, height = size
|
width, height = size
|
||||||
page_width_mm, page_height_mm = page_size
|
page_width_mm, page_height_mm = page_size
|
||||||
|
|
||||||
|
# Use project settings if available, otherwise use local settings
|
||||||
|
if project:
|
||||||
|
snap_threshold_mm = project.snap_threshold_mm
|
||||||
|
else:
|
||||||
|
snap_threshold_mm = self.snap_threshold_mm
|
||||||
|
|
||||||
# Convert threshold from mm to pixels
|
# Convert threshold from mm to pixels
|
||||||
snap_threshold_px = self.snap_threshold_mm * dpi / 25.4
|
snap_threshold_px = snap_threshold_mm * dpi / 25.4
|
||||||
|
|
||||||
# Calculate new position and size based on resize handle
|
# Calculate new position and size based on resize handle
|
||||||
new_x, new_y = x, y
|
new_x, new_y = x, y
|
||||||
@ -226,44 +250,44 @@ class SnappingSystem:
|
|||||||
if resize_handle in ['nw', 'w', 'sw']:
|
if resize_handle in ['nw', 'w', 'sw']:
|
||||||
# Try to snap the left edge
|
# Try to snap the left edge
|
||||||
snapped_left = self._snap_edge_to_targets(
|
snapped_left = self._snap_edge_to_targets(
|
||||||
new_x, page_width_mm, dpi, snap_threshold_px, 'vertical'
|
new_x, page_width_mm, dpi, snap_threshold_px, 'vertical', project
|
||||||
)
|
)
|
||||||
if snapped_left is not None:
|
if snapped_left is not None:
|
||||||
# Adjust width to compensate for position change
|
# Adjust width to compensate for position change
|
||||||
width_adjustment = new_x - snapped_left
|
width_adjustment = new_x - snapped_left
|
||||||
new_x = snapped_left
|
new_x = snapped_left
|
||||||
new_width += width_adjustment
|
new_width += width_adjustment
|
||||||
|
|
||||||
# Snap right edge (for ne, e, se handles)
|
# Snap right edge (for ne, e, se handles)
|
||||||
if resize_handle in ['ne', 'e', 'se']:
|
if resize_handle in ['ne', 'e', 'se']:
|
||||||
# Calculate right edge position
|
# Calculate right edge position
|
||||||
right_edge = new_x + new_width
|
right_edge = new_x + new_width
|
||||||
# Try to snap the right edge
|
# Try to snap the right edge
|
||||||
snapped_right = self._snap_edge_to_targets(
|
snapped_right = self._snap_edge_to_targets(
|
||||||
right_edge, page_width_mm, dpi, snap_threshold_px, 'vertical'
|
right_edge, page_width_mm, dpi, snap_threshold_px, 'vertical', project
|
||||||
)
|
)
|
||||||
if snapped_right is not None:
|
if snapped_right is not None:
|
||||||
new_width = snapped_right - new_x
|
new_width = snapped_right - new_x
|
||||||
|
|
||||||
# Snap top edge (for nw, n, ne handles)
|
# Snap top edge (for nw, n, ne handles)
|
||||||
if resize_handle in ['nw', 'n', 'ne']:
|
if resize_handle in ['nw', 'n', 'ne']:
|
||||||
# Try to snap the top edge
|
# Try to snap the top edge
|
||||||
snapped_top = self._snap_edge_to_targets(
|
snapped_top = self._snap_edge_to_targets(
|
||||||
new_y, page_height_mm, dpi, snap_threshold_px, 'horizontal'
|
new_y, page_height_mm, dpi, snap_threshold_px, 'horizontal', project
|
||||||
)
|
)
|
||||||
if snapped_top is not None:
|
if snapped_top is not None:
|
||||||
# Adjust height to compensate for position change
|
# Adjust height to compensate for position change
|
||||||
height_adjustment = new_y - snapped_top
|
height_adjustment = new_y - snapped_top
|
||||||
new_y = snapped_top
|
new_y = snapped_top
|
||||||
new_height += height_adjustment
|
new_height += height_adjustment
|
||||||
|
|
||||||
# Snap bottom edge (for sw, s, se handles)
|
# Snap bottom edge (for sw, s, se handles)
|
||||||
if resize_handle in ['sw', 's', 'se']:
|
if resize_handle in ['sw', 's', 'se']:
|
||||||
# Calculate bottom edge position
|
# Calculate bottom edge position
|
||||||
bottom_edge = new_y + new_height
|
bottom_edge = new_y + new_height
|
||||||
# Try to snap the bottom edge
|
# Try to snap the bottom edge
|
||||||
snapped_bottom = self._snap_edge_to_targets(
|
snapped_bottom = self._snap_edge_to_targets(
|
||||||
bottom_edge, page_height_mm, dpi, snap_threshold_px, 'horizontal'
|
bottom_edge, page_height_mm, dpi, snap_threshold_px, 'horizontal', project
|
||||||
)
|
)
|
||||||
if snapped_bottom is not None:
|
if snapped_bottom is not None:
|
||||||
new_height = snapped_bottom - new_y
|
new_height = snapped_bottom - new_y
|
||||||
@ -280,41 +304,55 @@ class SnappingSystem:
|
|||||||
page_size_mm: float,
|
page_size_mm: float,
|
||||||
dpi: int,
|
dpi: int,
|
||||||
snap_threshold_px: float,
|
snap_threshold_px: float,
|
||||||
orientation: str) -> Optional[float]:
|
orientation: str,
|
||||||
|
project=None) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
Snap an edge position to available targets (grid, edges, guides)
|
Snap an edge position to available targets (grid, edges, guides)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
edge_position: Current edge position in pixels
|
edge_position: Current edge position in pixels
|
||||||
page_size_mm: Page size along axis in mm
|
page_size_mm: Page size along axis in mm
|
||||||
dpi: DPI for conversion
|
dpi: DPI for conversion
|
||||||
snap_threshold_px: Snap threshold in pixels
|
snap_threshold_px: Snap threshold in pixels
|
||||||
orientation: 'vertical' for x-axis, 'horizontal' for y-axis
|
orientation: 'vertical' for x-axis, 'horizontal' for y-axis
|
||||||
|
project: Optional project for global snapping settings
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Snapped edge position in pixels, or None if no snap
|
Snapped edge position in pixels, or None if no snap
|
||||||
"""
|
"""
|
||||||
|
# Use project settings if available, otherwise use local settings
|
||||||
|
if project:
|
||||||
|
snap_to_grid = project.snap_to_grid
|
||||||
|
snap_to_edges = project.snap_to_edges
|
||||||
|
snap_to_guides = project.snap_to_guides
|
||||||
|
grid_size_mm = project.grid_size_mm
|
||||||
|
else:
|
||||||
|
snap_to_grid = self.snap_to_grid
|
||||||
|
snap_to_edges = self.snap_to_edges
|
||||||
|
snap_to_guides = self.snap_to_guides
|
||||||
|
grid_size_mm = self.grid_size_mm
|
||||||
|
|
||||||
snap_candidates = []
|
snap_candidates = []
|
||||||
|
|
||||||
# 1. Page edge snapping
|
# 1. Page edge snapping
|
||||||
if self.snap_to_edges:
|
if snap_to_edges:
|
||||||
# Snap to start edge (0)
|
# Snap to start edge (0)
|
||||||
snap_candidates.append((0, abs(edge_position - 0)))
|
snap_candidates.append((0, abs(edge_position - 0)))
|
||||||
|
|
||||||
# Snap to end edge
|
# Snap to end edge
|
||||||
page_size_px = page_size_mm * dpi / 25.4
|
page_size_px = page_size_mm * dpi / 25.4
|
||||||
snap_candidates.append((page_size_px, abs(edge_position - page_size_px)))
|
snap_candidates.append((page_size_px, abs(edge_position - page_size_px)))
|
||||||
|
|
||||||
# 2. Grid snapping
|
# 2. Grid snapping
|
||||||
if self.snap_to_grid:
|
if snap_to_grid:
|
||||||
grid_size_px = self.grid_size_mm * dpi / 25.4
|
grid_size_px = grid_size_mm * dpi / 25.4
|
||||||
|
|
||||||
# Snap to nearest grid line
|
# Snap to nearest grid line
|
||||||
nearest_grid = round(edge_position / grid_size_px) * grid_size_px
|
nearest_grid = round(edge_position / grid_size_px) * grid_size_px
|
||||||
snap_candidates.append((nearest_grid, abs(edge_position - nearest_grid)))
|
snap_candidates.append((nearest_grid, abs(edge_position - nearest_grid)))
|
||||||
|
|
||||||
# 3. Guide snapping
|
# 3. Guide snapping
|
||||||
if self.snap_to_guides:
|
if snap_to_guides:
|
||||||
for guide in self.guides:
|
for guide in self.guides:
|
||||||
if guide.orientation == orientation:
|
if guide.orientation == orientation:
|
||||||
guide_pos_px = guide.position * dpi / 25.4
|
guide_pos_px = guide.position * dpi / 25.4
|
||||||
|
|||||||
50
test_drop_bug.py
Normal file
50
test_drop_bug.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test to demonstrate the asset drop bug
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from pyPhotoAlbum.project import Project, Page
|
||||||
|
from pyPhotoAlbum.models import ImageData
|
||||||
|
|
||||||
|
def test_direct_path_assignment():
|
||||||
|
"""Simulate what happens when you drop an image on existing element"""
|
||||||
|
|
||||||
|
project = Project("Test Direct Path")
|
||||||
|
page = Page()
|
||||||
|
project.add_page(page)
|
||||||
|
|
||||||
|
# Add an image element
|
||||||
|
img = ImageData()
|
||||||
|
img.position = (10, 10)
|
||||||
|
img.size = (50, 50)
|
||||||
|
page.layout.add_element(img)
|
||||||
|
|
||||||
|
# Simulate dropping a new image on existing element (line 77 in asset_drop.py)
|
||||||
|
external_image = "/home/dtourolle/Pictures/some_photo.jpg"
|
||||||
|
print(f"\nSimulating drop on existing image element...")
|
||||||
|
print(f"Setting image_path directly to: {external_image}")
|
||||||
|
img.image_path = external_image # BUG: Not imported!
|
||||||
|
|
||||||
|
# Check assets folder
|
||||||
|
assets = os.listdir(project.asset_manager.assets_folder) if os.path.exists(project.asset_manager.assets_folder) else []
|
||||||
|
print(f"\nAssets in folder: {len(assets)}")
|
||||||
|
print(f" {assets if assets else '(empty)'}")
|
||||||
|
|
||||||
|
# The image path in the element points to external file
|
||||||
|
print(f"\nImage path in element: {img.image_path}")
|
||||||
|
print(f" Is absolute path: {os.path.isabs(img.image_path)}")
|
||||||
|
|
||||||
|
if os.path.isabs(img.image_path):
|
||||||
|
print("\n❌ BUG CONFIRMED: Image path is absolute, not copied to assets!")
|
||||||
|
print(" When saved to .ppz, this external file will NOT be included.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_direct_path_assignment()
|
||||||
155
test_heal_function.py
Executable file
155
test_heal_function.py
Executable file
@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test to verify the heal function can fix old files with missing assets
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from pyPhotoAlbum.project import Project, Page
|
||||||
|
from pyPhotoAlbum.models import ImageData
|
||||||
|
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip
|
||||||
|
|
||||||
|
def test_heal_external_paths():
|
||||||
|
"""Test healing a project with external (absolute) image paths"""
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("Test: Healing External Image Paths")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Create a test image in a temp location
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
# Create a test image file
|
||||||
|
test_image_source = "./projects/project_with_image/assets/test_image.jpg"
|
||||||
|
external_image_path = os.path.join(temp_dir, "external_photo.jpg")
|
||||||
|
shutil.copy2(test_image_source, external_image_path)
|
||||||
|
|
||||||
|
print(f"\n1. Created external image at: {external_image_path}")
|
||||||
|
|
||||||
|
# Create a project
|
||||||
|
project = Project("Test Heal")
|
||||||
|
page = Page()
|
||||||
|
project.add_page(page)
|
||||||
|
|
||||||
|
# Add an image element with ABSOLUTE path (simulating the bug)
|
||||||
|
img = ImageData()
|
||||||
|
img.image_path = external_image_path # BUG: Absolute path!
|
||||||
|
img.position = (10, 10)
|
||||||
|
img.size = (50, 50)
|
||||||
|
page.layout.add_element(img)
|
||||||
|
|
||||||
|
print(f"2. Created project with absolute path: {img.image_path}")
|
||||||
|
|
||||||
|
# Save to zip
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.ppz', delete=False) as tmp:
|
||||||
|
zip_path = tmp.name
|
||||||
|
|
||||||
|
save_to_zip(project, zip_path)
|
||||||
|
print(f"3. Saved to: {zip_path}")
|
||||||
|
|
||||||
|
# Check what was saved
|
||||||
|
import zipfile
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zf:
|
||||||
|
files = zf.namelist()
|
||||||
|
asset_files = [f for f in files if f.startswith('assets/')]
|
||||||
|
print(f"\n4. Assets in zip: {len(asset_files)}")
|
||||||
|
if len(asset_files) == 0:
|
||||||
|
print(" ❌ No assets saved (expected - this is the bug!)")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Assets: {asset_files}")
|
||||||
|
|
||||||
|
# Load the project
|
||||||
|
loaded_project, error = load_from_zip(zip_path)
|
||||||
|
if not loaded_project:
|
||||||
|
print(f" ❌ Failed to load: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"\n5. Loaded project from zip")
|
||||||
|
|
||||||
|
# Check for missing assets
|
||||||
|
from pyPhotoAlbum.models import ImageData as ImageDataCheck
|
||||||
|
missing_count = 0
|
||||||
|
for page in loaded_project.pages:
|
||||||
|
for element in page.layout.elements:
|
||||||
|
if isinstance(element, ImageDataCheck) and element.image_path:
|
||||||
|
if os.path.isabs(element.image_path):
|
||||||
|
full_path = element.image_path
|
||||||
|
else:
|
||||||
|
full_path = os.path.join(loaded_project.folder_path, element.image_path)
|
||||||
|
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
missing_count += 1
|
||||||
|
print(f" ❌ Missing: {element.image_path}")
|
||||||
|
|
||||||
|
print(f"\n6. Missing assets detected: {missing_count}")
|
||||||
|
|
||||||
|
if missing_count == 0:
|
||||||
|
print(" ⚠️ No missing assets - test may not be accurate")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Now test the healing logic
|
||||||
|
print(f"\n7. Testing heal function...")
|
||||||
|
|
||||||
|
# Simulate what the heal dialog does
|
||||||
|
from pyPhotoAlbum.models import set_asset_resolution_context
|
||||||
|
|
||||||
|
search_paths = [temp_dir] # The directory where our external image is
|
||||||
|
set_asset_resolution_context(loaded_project.folder_path, search_paths)
|
||||||
|
|
||||||
|
healed_count = 0
|
||||||
|
for page in loaded_project.pages:
|
||||||
|
for element in page.layout.elements:
|
||||||
|
if isinstance(element, ImageDataCheck) and element.image_path:
|
||||||
|
# Check if missing
|
||||||
|
if os.path.isabs(element.image_path):
|
||||||
|
full_path = element.image_path
|
||||||
|
else:
|
||||||
|
full_path = os.path.join(loaded_project.folder_path, element.image_path)
|
||||||
|
|
||||||
|
if not os.path.exists(full_path):
|
||||||
|
# Try to find and import
|
||||||
|
filename = os.path.basename(element.image_path)
|
||||||
|
for search_path in search_paths:
|
||||||
|
candidate = os.path.join(search_path, filename)
|
||||||
|
if os.path.exists(candidate):
|
||||||
|
# Import it!
|
||||||
|
new_asset_path = loaded_project.asset_manager.import_asset(candidate)
|
||||||
|
element.image_path = new_asset_path
|
||||||
|
healed_count += 1
|
||||||
|
print(f" ✅ Healed: {filename} → {new_asset_path}")
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"\n8. Healed {healed_count} asset(s)")
|
||||||
|
|
||||||
|
# Save the healed project
|
||||||
|
healed_zip_path = zip_path.replace('.ppz', '_healed.ppz')
|
||||||
|
save_to_zip(loaded_project, healed_zip_path)
|
||||||
|
print(f"9. Saved healed project to: {healed_zip_path}")
|
||||||
|
|
||||||
|
# Verify the healed version has assets
|
||||||
|
with zipfile.ZipFile(healed_zip_path, 'r') as zf:
|
||||||
|
files = zf.namelist()
|
||||||
|
asset_files = [f for f in files if f.startswith('assets/')]
|
||||||
|
print(f"\n10. Assets in healed zip: {len(asset_files)}")
|
||||||
|
for asset in asset_files:
|
||||||
|
print(f" - {asset}")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
loaded_project.cleanup()
|
||||||
|
os.unlink(zip_path)
|
||||||
|
os.unlink(healed_zip_path)
|
||||||
|
|
||||||
|
if healed_count > 0 and len(asset_files) > 0:
|
||||||
|
print("\n✅ HEAL FUNCTION WORKS - Old files can be fixed!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print("\n❌ Healing did not work as expected")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = test_heal_external_paths()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
139
test_zip_embedding.py
Executable file
139
test_zip_embedding.py
Executable file
@ -0,0 +1,139 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify that images are being embedded in .ppz files
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
# Add project to path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from pyPhotoAlbum.project import Project, Page
|
||||||
|
from pyPhotoAlbum.models import ImageData
|
||||||
|
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip
|
||||||
|
|
||||||
|
def test_zip_embedding():
|
||||||
|
"""Test that images are properly embedded in the zip file"""
|
||||||
|
|
||||||
|
# Create a test project with an image
|
||||||
|
project = Project("Test Embedding")
|
||||||
|
page = Page()
|
||||||
|
project.add_page(page)
|
||||||
|
|
||||||
|
# Use an existing test image
|
||||||
|
test_image = "./projects/project_with_image/assets/test_image.jpg"
|
||||||
|
if not os.path.exists(test_image):
|
||||||
|
print(f"ERROR: Test image not found: {test_image}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Using test image: {test_image}")
|
||||||
|
print(f"Test image size: {os.path.getsize(test_image)} bytes")
|
||||||
|
|
||||||
|
# Import the image through the asset manager
|
||||||
|
print("\n1. Importing image through asset manager...")
|
||||||
|
asset_path = project.asset_manager.import_asset(test_image)
|
||||||
|
print(f" Asset path: {asset_path}")
|
||||||
|
|
||||||
|
# Check that the asset was copied
|
||||||
|
full_asset_path = project.asset_manager.get_absolute_path(asset_path)
|
||||||
|
if os.path.exists(full_asset_path):
|
||||||
|
print(f" ✓ Asset exists at: {full_asset_path}")
|
||||||
|
print(f" Asset size: {os.path.getsize(full_asset_path)} bytes")
|
||||||
|
else:
|
||||||
|
print(f" ✗ ERROR: Asset not found at {full_asset_path}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Add image to page
|
||||||
|
img = ImageData()
|
||||||
|
img.image_path = asset_path
|
||||||
|
img.position = (10, 10)
|
||||||
|
img.size = (50, 50)
|
||||||
|
page.layout.add_element(img)
|
||||||
|
|
||||||
|
# Save to a temporary zip file
|
||||||
|
print("\n2. Saving project to .ppz file...")
|
||||||
|
with tempfile.NamedTemporaryFile(suffix='.ppz', delete=False) as tmp:
|
||||||
|
zip_path = tmp.name
|
||||||
|
|
||||||
|
success, error = save_to_zip(project, zip_path)
|
||||||
|
if not success:
|
||||||
|
print(f" ✗ ERROR: Failed to save: {error}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" ✓ Saved to: {zip_path}")
|
||||||
|
zip_size = os.path.getsize(zip_path)
|
||||||
|
print(f" Zip file size: {zip_size:,} bytes")
|
||||||
|
|
||||||
|
# Inspect the zip file contents
|
||||||
|
print("\n3. Inspecting zip file contents...")
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zf:
|
||||||
|
files = zf.namelist()
|
||||||
|
print(f" Files in zip: {len(files)}")
|
||||||
|
for fname in files:
|
||||||
|
info = zf.getinfo(fname)
|
||||||
|
print(f" - {fname} ({info.file_size:,} bytes)")
|
||||||
|
|
||||||
|
# Check if assets are included
|
||||||
|
asset_files = [f for f in files if f.startswith('assets/')]
|
||||||
|
print(f"\n4. Checking for embedded assets...")
|
||||||
|
print(f" Assets found: {len(asset_files)}")
|
||||||
|
|
||||||
|
if len(asset_files) == 0:
|
||||||
|
print(" ✗ ERROR: No assets embedded in zip file!")
|
||||||
|
print(f"\n DEBUG INFO:")
|
||||||
|
print(f" Project folder: {project.folder_path}")
|
||||||
|
print(f" Assets folder: {project.asset_manager.assets_folder}")
|
||||||
|
print(f" Assets folder exists: {os.path.exists(project.asset_manager.assets_folder)}")
|
||||||
|
|
||||||
|
if os.path.exists(project.asset_manager.assets_folder):
|
||||||
|
assets = os.listdir(project.asset_manager.assets_folder)
|
||||||
|
print(f" Files in assets folder: {assets}")
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
os.unlink(zip_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" ✓ Found {len(asset_files)} asset file(s) in zip")
|
||||||
|
|
||||||
|
# Load the project back
|
||||||
|
print("\n5. Loading project from zip...")
|
||||||
|
loaded_project, error = load_from_zip(zip_path)
|
||||||
|
if loaded_project is None:
|
||||||
|
print(f" ✗ ERROR: Failed to load: {error}")
|
||||||
|
os.unlink(zip_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f" ✓ Loaded project: {loaded_project.name}")
|
||||||
|
|
||||||
|
# Check that the image is accessible
|
||||||
|
print("\n6. Verifying loaded image...")
|
||||||
|
if loaded_project.pages and loaded_project.pages[0].layout.elements:
|
||||||
|
img_elem = loaded_project.pages[0].layout.elements[0]
|
||||||
|
if isinstance(img_elem, ImageData):
|
||||||
|
loaded_img_path = loaded_project.asset_manager.get_absolute_path(img_elem.image_path)
|
||||||
|
if os.path.exists(loaded_img_path):
|
||||||
|
print(f" ✓ Image accessible at: {loaded_img_path}")
|
||||||
|
print(f" Image size: {os.path.getsize(loaded_img_path)} bytes")
|
||||||
|
else:
|
||||||
|
print(f" ✗ ERROR: Image not found at {loaded_img_path}")
|
||||||
|
os.unlink(zip_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
os.unlink(zip_path)
|
||||||
|
loaded_project.cleanup()
|
||||||
|
|
||||||
|
print("\n✅ ALL TESTS PASSED - Images are being embedded correctly!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 70)
|
||||||
|
print("Testing .ppz file image embedding")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
success = test_zip_embedding()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
Loading…
x
Reference in New Issue
Block a user