fixed bug with images being displayed squashed when dimensions exceeded render size
This commit is contained in:
parent
7d8d2d42f8
commit
1b0e6f9ed1
@ -354,12 +354,16 @@ class AsyncImageLoader(QObject):
|
|||||||
if img.mode != 'RGBA':
|
if img.mode != 'RGBA':
|
||||||
img = img.convert('RGBA')
|
img = img.convert('RGBA')
|
||||||
|
|
||||||
# Downsample if target size specified
|
# Downsample if target size specified (preserving aspect ratio)
|
||||||
if target_size:
|
if target_size:
|
||||||
current_size = img.size
|
max_size = target_size[0] # Assume square target (2048, 2048)
|
||||||
if current_size[0] > target_size[0] or current_size[1] > target_size[1]:
|
if img.width > max_size or img.height > max_size:
|
||||||
img = img.resize(target_size, Image.Resampling.LANCZOS)
|
# Calculate scale to fit within max_size while preserving aspect ratio
|
||||||
logger.debug(f"Downsampled {path}: {current_size} -> {target_size}")
|
scale = min(max_size / img.width, max_size / img.height)
|
||||||
|
new_width = int(img.width * scale)
|
||||||
|
new_height = int(img.height * scale)
|
||||||
|
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
||||||
|
logger.debug(f"Downsampled {path}: {img.size} -> ({new_width}, {new_height})")
|
||||||
|
|
||||||
return img
|
return img
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
|||||||
from typing import Tuple, Optional, Dict, Any, List
|
from typing import Tuple, Optional, Dict, Any, List
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
# Global configuration for asset path resolution
|
# Global configuration for asset path resolution
|
||||||
_asset_search_paths: List[str] = []
|
_asset_search_paths: List[str] = []
|
||||||
@ -57,15 +58,55 @@ class BaseLayoutElement(ABC):
|
|||||||
class ImageData(BaseLayoutElement):
|
class ImageData(BaseLayoutElement):
|
||||||
"""Class to store image data and properties"""
|
"""Class to store image data and properties"""
|
||||||
|
|
||||||
def __init__(self, image_path: str = "", crop_info: Optional[Tuple] = None, **kwargs):
|
def __init__(self, image_path: str = "", crop_info: Optional[Tuple] = None,
|
||||||
|
image_dimensions: Optional[Tuple[int, int]] = None, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.image_path = image_path
|
self.image_path = image_path
|
||||||
self.crop_info = crop_info or (0, 0, 1, 1) # Default: no crop
|
self.crop_info = crop_info or (0, 0, 1, 1) # Default: no crop
|
||||||
|
|
||||||
|
# Metadata: Store image dimensions for aspect ratio calculations before full load
|
||||||
|
# This allows correct rendering even while async loading is in progress
|
||||||
|
self.image_dimensions = image_dimensions # (width, height) or None
|
||||||
|
|
||||||
|
# If dimensions not provided and we have a path, try to extract them quickly
|
||||||
|
if not self.image_dimensions and self.image_path:
|
||||||
|
self._extract_dimensions_metadata()
|
||||||
|
|
||||||
# Async loading state
|
# Async loading state
|
||||||
self._async_loading = False
|
self._async_loading = False
|
||||||
self._async_load_requested = False
|
self._async_load_requested = False
|
||||||
|
|
||||||
|
def _extract_dimensions_metadata(self):
|
||||||
|
"""
|
||||||
|
Extract image dimensions without loading the full image.
|
||||||
|
Uses PIL's lazy loading to just read the header.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Resolve path
|
||||||
|
image_path = self.image_path
|
||||||
|
if not os.path.isabs(self.image_path):
|
||||||
|
project_folder, search_paths = get_asset_search_paths()
|
||||||
|
if project_folder and os.path.exists(os.path.join(project_folder, self.image_path)):
|
||||||
|
image_path = os.path.join(project_folder, self.image_path)
|
||||||
|
|
||||||
|
if os.path.exists(image_path):
|
||||||
|
# Use PIL to just read dimensions (fast, doesn't load pixel data)
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
width, height = img.width, img.height
|
||||||
|
|
||||||
|
# Apply same downsampling logic as the old sync code (max 2048px)
|
||||||
|
max_size = 2048
|
||||||
|
if width > max_size or height > max_size:
|
||||||
|
scale = min(max_size / width, max_size / height)
|
||||||
|
width = int(width * scale)
|
||||||
|
height = int(height * scale)
|
||||||
|
|
||||||
|
self.image_dimensions = (width, height)
|
||||||
|
print(f"ImageData: Extracted dimensions {self.image_dimensions} for {self.image_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ImageData: Could not extract dimensions for {self.image_path}: {e}")
|
||||||
|
self.image_dimensions = None
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
"""Render the image using OpenGL"""
|
"""Render the image using OpenGL"""
|
||||||
from OpenGL.GL import (glBegin, glEnd, glVertex2f, glColor3f, glColor4f, GL_QUADS, GL_LINE_LOOP,
|
from OpenGL.GL import (glBegin, glEnd, glVertex2f, glColor3f, glColor4f, GL_QUADS, GL_LINE_LOOP,
|
||||||
@ -126,55 +167,29 @@ class ImageData(BaseLayoutElement):
|
|||||||
print(f"ImageData: Could not resolve path: {self.image_path}")
|
print(f"ImageData: Could not resolve path: {self.image_path}")
|
||||||
print(f" Tried paths: {possible_paths[:3]}") # Print first 3 to avoid clutter
|
print(f" Tried paths: {possible_paths[:3]}") # Print first 3 to avoid clutter
|
||||||
|
|
||||||
if image_full_path and os.path.exists(image_full_path):
|
# NOTE: Async loading is now handled by page_layout.py calling request_image_load()
|
||||||
# Check if we already have a cached texture
|
# This sync path should only be reached if async loading is not available
|
||||||
if not hasattr(self, '_texture_id') or not hasattr(self, '_texture_path') or self._texture_path != image_full_path:
|
# The actual image will be loaded in the background and the texture created
|
||||||
try:
|
# via _on_async_image_loaded() callback when ready
|
||||||
# Load image
|
|
||||||
img = Image.open(image_full_path)
|
|
||||||
img = img.convert('RGBA')
|
|
||||||
|
|
||||||
# Scale down large images for performance (max 2048px on longest side)
|
# Use cached texture if available
|
||||||
max_size = 2048
|
|
||||||
if img.width > max_size or img.height > max_size:
|
|
||||||
scale = min(max_size / img.width, max_size / img.height)
|
|
||||||
new_width = int(img.width * scale)
|
|
||||||
new_height = int(img.height * scale)
|
|
||||||
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
img_data = img.tobytes()
|
|
||||||
|
|
||||||
# Delete old texture if it exists
|
|
||||||
if hasattr(self, '_texture_id') and self._texture_id:
|
|
||||||
glDeleteTextures([self._texture_id])
|
|
||||||
|
|
||||||
# Create texture
|
|
||||||
texture_id = glGenTextures(1)
|
|
||||||
glBindTexture(GL_TEXTURE_2D, texture_id)
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
|
|
||||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, img.width, img.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)
|
|
||||||
|
|
||||||
# Cache texture ID and path
|
|
||||||
self._texture_id = texture_id
|
|
||||||
self._texture_path = image_full_path
|
|
||||||
self._img_width = img.width
|
|
||||||
self._img_height = img.height
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading image {image_full_path}: {e}")
|
|
||||||
print(f" Original path: {self.image_path}")
|
|
||||||
self._texture_id = None
|
|
||||||
|
|
||||||
# Use cached texture
|
|
||||||
if hasattr(self, '_texture_id') and self._texture_id:
|
if hasattr(self, '_texture_id') and self._texture_id:
|
||||||
texture_id = self._texture_id
|
texture_id = self._texture_id
|
||||||
|
|
||||||
|
# Get image dimensions (from loaded texture or metadata)
|
||||||
|
if hasattr(self, '_img_width') and hasattr(self, '_img_height'):
|
||||||
|
img_width, img_height = self._img_width, self._img_height
|
||||||
|
elif self.image_dimensions:
|
||||||
|
img_width, img_height = self.image_dimensions
|
||||||
|
else:
|
||||||
|
# No dimensions available, render without aspect ratio correction
|
||||||
|
img_width, img_height = int(w), int(h)
|
||||||
|
|
||||||
# Get crop info
|
# Get crop info
|
||||||
crop_x_min, crop_y_min, crop_x_max, crop_y_max = self.crop_info
|
crop_x_min, crop_y_min, crop_x_max, crop_y_max = self.crop_info
|
||||||
|
|
||||||
# Calculate aspect ratios for center crop
|
# Calculate aspect ratios for center crop
|
||||||
img_aspect = self._img_width / self._img_height
|
img_aspect = img_width / img_height
|
||||||
target_aspect = w / h
|
target_aspect = w / h
|
||||||
|
|
||||||
# Calculate texture coordinates for center crop
|
# Calculate texture coordinates for center crop
|
||||||
@ -243,7 +258,7 @@ class ImageData(BaseLayoutElement):
|
|||||||
|
|
||||||
def serialize(self) -> Dict[str, Any]:
|
def serialize(self) -> Dict[str, Any]:
|
||||||
"""Serialize image data to dictionary"""
|
"""Serialize image data to dictionary"""
|
||||||
return {
|
data = {
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"position": self.position,
|
"position": self.position,
|
||||||
"size": self.size,
|
"size": self.size,
|
||||||
@ -252,6 +267,10 @@ class ImageData(BaseLayoutElement):
|
|||||||
"image_path": self.image_path,
|
"image_path": self.image_path,
|
||||||
"crop_info": self.crop_info
|
"crop_info": self.crop_info
|
||||||
}
|
}
|
||||||
|
# Include image dimensions metadata if available
|
||||||
|
if self.image_dimensions:
|
||||||
|
data["image_dimensions"] = self.image_dimensions
|
||||||
|
return data
|
||||||
|
|
||||||
def deserialize(self, data: Dict[str, Any]):
|
def deserialize(self, data: Dict[str, Any]):
|
||||||
"""Deserialize from dictionary"""
|
"""Deserialize from dictionary"""
|
||||||
@ -261,6 +280,10 @@ class ImageData(BaseLayoutElement):
|
|||||||
self.z_index = data.get("z_index", 0)
|
self.z_index = data.get("z_index", 0)
|
||||||
self.image_path = data.get("image_path", "")
|
self.image_path = data.get("image_path", "")
|
||||||
self.crop_info = tuple(data.get("crop_info", (0, 0, 1, 1)))
|
self.crop_info = tuple(data.get("crop_info", (0, 0, 1, 1)))
|
||||||
|
# Load image dimensions metadata if available
|
||||||
|
self.image_dimensions = data.get("image_dimensions", None)
|
||||||
|
if self.image_dimensions:
|
||||||
|
self.image_dimensions = tuple(self.image_dimensions)
|
||||||
|
|
||||||
def _on_async_image_loaded(self, pil_image):
|
def _on_async_image_loaded(self, pil_image):
|
||||||
"""
|
"""
|
||||||
@ -295,6 +318,10 @@ class ImageData(BaseLayoutElement):
|
|||||||
self._img_height = pil_image.height
|
self._img_height = pil_image.height
|
||||||
self._async_loading = False
|
self._async_loading = False
|
||||||
|
|
||||||
|
# Update metadata for future renders
|
||||||
|
if not self.image_dimensions:
|
||||||
|
self.image_dimensions = (pil_image.width, pil_image.height)
|
||||||
|
|
||||||
print(f"ImageData: Async loaded texture for {self.image_path}")
|
print(f"ImageData: Async loaded texture for {self.image_path}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user