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':
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# Downsample if target size specified
|
||||
# Downsample if target size specified (preserving aspect ratio)
|
||||
if target_size:
|
||||
current_size = img.size
|
||||
if current_size[0] > target_size[0] or current_size[1] > target_size[1]:
|
||||
img = img.resize(target_size, Image.Resampling.LANCZOS)
|
||||
logger.debug(f"Downsampled {path}: {current_size} -> {target_size}")
|
||||
max_size = target_size[0] # Assume square target (2048, 2048)
|
||||
if img.width > max_size or img.height > max_size:
|
||||
# Calculate scale to fit within max_size while preserving aspect ratio
|
||||
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
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
||||
from typing import Tuple, Optional, Dict, Any, List
|
||||
import json
|
||||
import os
|
||||
from PIL import Image
|
||||
|
||||
# Global configuration for asset path resolution
|
||||
_asset_search_paths: List[str] = []
|
||||
@ -57,15 +58,55 @@ class BaseLayoutElement(ABC):
|
||||
class ImageData(BaseLayoutElement):
|
||||
"""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)
|
||||
self.image_path = image_path
|
||||
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
|
||||
self._async_loading = 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):
|
||||
"""Render the image using OpenGL"""
|
||||
from OpenGL.GL import (glBegin, glEnd, glVertex2f, glColor3f, glColor4f, GL_QUADS, GL_LINE_LOOP,
|
||||
@ -126,97 +167,71 @@ class ImageData(BaseLayoutElement):
|
||||
print(f"ImageData: Could not resolve path: {self.image_path}")
|
||||
print(f" Tried paths: {possible_paths[:3]}") # Print first 3 to avoid clutter
|
||||
|
||||
if image_full_path and os.path.exists(image_full_path):
|
||||
# Check if we already have a cached texture
|
||||
if not hasattr(self, '_texture_id') or not hasattr(self, '_texture_path') or self._texture_path != image_full_path:
|
||||
try:
|
||||
# Load image
|
||||
img = Image.open(image_full_path)
|
||||
img = img.convert('RGBA')
|
||||
|
||||
# Scale down large images for performance (max 2048px on longest side)
|
||||
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:
|
||||
texture_id = self._texture_id
|
||||
|
||||
# Get crop info
|
||||
crop_x_min, crop_y_min, crop_x_max, crop_y_max = self.crop_info
|
||||
|
||||
# Calculate aspect ratios for center crop
|
||||
img_aspect = self._img_width / self._img_height
|
||||
target_aspect = w / h
|
||||
|
||||
# Calculate texture coordinates for center crop
|
||||
if img_aspect > target_aspect:
|
||||
# Image is wider - crop horizontally
|
||||
scale = target_aspect / img_aspect
|
||||
tx_offset = (1.0 - scale) / 2.0
|
||||
tx_min_base = tx_offset
|
||||
tx_max_base = 1.0 - tx_offset
|
||||
ty_min_base = 0.0
|
||||
ty_max_base = 1.0
|
||||
else:
|
||||
# Image is taller - crop vertically
|
||||
scale = img_aspect / target_aspect
|
||||
ty_offset = (1.0 - scale) / 2.0
|
||||
tx_min_base = 0.0
|
||||
tx_max_base = 1.0
|
||||
ty_min_base = ty_offset
|
||||
ty_max_base = 1.0 - ty_offset
|
||||
|
||||
# Apply additional crop from crop_info (for spanning elements)
|
||||
tx_range = tx_max_base - tx_min_base
|
||||
ty_range = ty_max_base - ty_min_base
|
||||
|
||||
tx_min = tx_min_base + crop_x_min * tx_range
|
||||
tx_max = tx_min_base + crop_x_max * tx_range
|
||||
ty_min = ty_min_base + crop_y_min * ty_range
|
||||
ty_max = ty_min_base + crop_y_max * ty_range
|
||||
|
||||
# Enable texturing and draw with crop
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
glBindTexture(GL_TEXTURE_2D, texture_id)
|
||||
glColor4f(1.0, 1.0, 1.0, 1.0) # White color to show texture as-is
|
||||
|
||||
glBegin(GL_QUADS)
|
||||
glTexCoord2f(tx_min, ty_min); glVertex2f(x, y)
|
||||
glTexCoord2f(tx_max, ty_min); glVertex2f(x + w, y)
|
||||
glTexCoord2f(tx_max, ty_max); glVertex2f(x + w, y + h)
|
||||
glTexCoord2f(tx_min, ty_max); glVertex2f(x, y + h)
|
||||
glEnd()
|
||||
|
||||
glDisable(GL_TEXTURE_2D)
|
||||
# NOTE: Async loading is now handled by page_layout.py calling request_image_load()
|
||||
# This sync path should only be reached if async loading is not available
|
||||
# The actual image will be loaded in the background and the texture created
|
||||
# via _on_async_image_loaded() callback when ready
|
||||
|
||||
# Use cached texture if available
|
||||
if hasattr(self, '_texture_id') and 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
|
||||
crop_x_min, crop_y_min, crop_x_max, crop_y_max = self.crop_info
|
||||
|
||||
# Calculate aspect ratios for center crop
|
||||
img_aspect = img_width / img_height
|
||||
target_aspect = w / h
|
||||
|
||||
# Calculate texture coordinates for center crop
|
||||
if img_aspect > target_aspect:
|
||||
# Image is wider - crop horizontally
|
||||
scale = target_aspect / img_aspect
|
||||
tx_offset = (1.0 - scale) / 2.0
|
||||
tx_min_base = tx_offset
|
||||
tx_max_base = 1.0 - tx_offset
|
||||
ty_min_base = 0.0
|
||||
ty_max_base = 1.0
|
||||
else:
|
||||
# Image is taller - crop vertically
|
||||
scale = img_aspect / target_aspect
|
||||
ty_offset = (1.0 - scale) / 2.0
|
||||
tx_min_base = 0.0
|
||||
tx_max_base = 1.0
|
||||
ty_min_base = ty_offset
|
||||
ty_max_base = 1.0 - ty_offset
|
||||
|
||||
# Apply additional crop from crop_info (for spanning elements)
|
||||
tx_range = tx_max_base - tx_min_base
|
||||
ty_range = ty_max_base - ty_min_base
|
||||
|
||||
tx_min = tx_min_base + crop_x_min * tx_range
|
||||
tx_max = tx_min_base + crop_x_max * tx_range
|
||||
ty_min = ty_min_base + crop_y_min * ty_range
|
||||
ty_max = ty_min_base + crop_y_max * ty_range
|
||||
|
||||
# Enable texturing and draw with crop
|
||||
glEnable(GL_TEXTURE_2D)
|
||||
glBindTexture(GL_TEXTURE_2D, texture_id)
|
||||
glColor4f(1.0, 1.0, 1.0, 1.0) # White color to show texture as-is
|
||||
|
||||
glBegin(GL_QUADS)
|
||||
glTexCoord2f(tx_min, ty_min); glVertex2f(x, y)
|
||||
glTexCoord2f(tx_max, ty_min); glVertex2f(x + w, y)
|
||||
glTexCoord2f(tx_max, ty_max); glVertex2f(x + w, y + h)
|
||||
glTexCoord2f(tx_min, ty_max); glVertex2f(x, y + h)
|
||||
glEnd()
|
||||
|
||||
glDisable(GL_TEXTURE_2D)
|
||||
|
||||
# If no image or loading failed, draw placeholder
|
||||
if not texture_id:
|
||||
@ -243,7 +258,7 @@ class ImageData(BaseLayoutElement):
|
||||
|
||||
def serialize(self) -> Dict[str, Any]:
|
||||
"""Serialize image data to dictionary"""
|
||||
return {
|
||||
data = {
|
||||
"type": "image",
|
||||
"position": self.position,
|
||||
"size": self.size,
|
||||
@ -252,6 +267,10 @@ class ImageData(BaseLayoutElement):
|
||||
"image_path": self.image_path,
|
||||
"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]):
|
||||
"""Deserialize from dictionary"""
|
||||
@ -261,6 +280,10 @@ class ImageData(BaseLayoutElement):
|
||||
self.z_index = data.get("z_index", 0)
|
||||
self.image_path = data.get("image_path", "")
|
||||
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):
|
||||
"""
|
||||
@ -295,6 +318,10 @@ class ImageData(BaseLayoutElement):
|
||||
self._img_height = pil_image.height
|
||||
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}")
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user