269 lines
No EOL
12 KiB
Python
269 lines
No EOL
12 KiB
Python
import os
|
|
import json
|
|
import logging
|
|
from typing import Dict, Any, Set, Tuple, Optional
|
|
from PIL import Image
|
|
from utils.check_helpers import record_skipped_type, prepare_skipped_result
|
|
|
|
def run_check(config):
|
|
"""
|
|
Check that each image in linkingrecord.json matches the expected file format (PNG, JPG, or AVIF)
|
|
based on the viewtype and imagetype conditions.
|
|
|
|
Expected config:
|
|
- working_dir (str): Directory where linkingrecord.json and images reside.
|
|
- linkingrecord_filename (str): Name of the linking record file. Default: linkingrecord.json
|
|
|
|
Returns:
|
|
- "passed" if all checked images match their expected format
|
|
- "passed" with skipped_types details if unknown type combinations were encountered
|
|
- "failed" if any present images don't match, listing each that failed
|
|
- "error" if linkingrecord is missing or invalid
|
|
"""
|
|
|
|
working_dir = config.get("working_dir", "working")
|
|
linkingrecord_filename = config.get("linkingrecord_filename", "linkingrecord.json")
|
|
linkingrecord_path = os.path.join(working_dir, linkingrecord_filename)
|
|
|
|
if not os.path.exists(linkingrecord_path):
|
|
return {
|
|
"status": "error",
|
|
"error_message": f"Linking record '{linkingrecord_filename}' not found in {working_dir}."
|
|
}
|
|
|
|
# Load linkingrecord.json
|
|
with open(linkingrecord_path, 'r', encoding='utf-8') as f:
|
|
linkingrecord = json.load(f)
|
|
|
|
if "items" not in linkingrecord or not isinstance(linkingrecord["items"], list):
|
|
return {
|
|
"status": "error",
|
|
"error_message": "Invalid linkingrecord.json structure: 'items' missing or not a list."
|
|
}
|
|
|
|
# Detect if this is a MEC pack (contains experienceCondition="2d-background")
|
|
is_mec = any(
|
|
item.get("conditions", {}).get("experienceCondition") == "2d-background"
|
|
for item in linkingrecord["items"]
|
|
)
|
|
pack_type = "MEC" if is_mec else "BAU"
|
|
|
|
# Define format requirements (base assets handled separately)
|
|
# Format: (viewtype, imagetype): expected_format
|
|
format_requirements = {
|
|
# Base assets ('exterior', None) and ('interior', None) are handled with flexible logic below
|
|
('exterior', 'layeroptext'): 'PNG',
|
|
('interior', 'layeroptint'): 'PNG',
|
|
('carousel', 'extra'): 'JPEG',
|
|
('carousel', 'powertrain'): 'JPEG',
|
|
('exterior', 'showroom'): 'PNG',
|
|
('carousel', 'colour'): 'JPEG',
|
|
('carousel', 'bodystyle'): 'JPEG',
|
|
('carousel', 'series'): 'JPEG',
|
|
('carousel', 'trim'): 'JPEG',
|
|
# Adding lifestyle and inventory requirements
|
|
('lifestyle', None): 'JPEG',
|
|
('inventory', None): 'JPEG',
|
|
# Adding beltline requirements (desktop and mobile variants)
|
|
('vehicleselector', 'desktop'): 'AVIF',
|
|
('vehicleselector', 'mobile'): 'AVIF',
|
|
}
|
|
|
|
def is_base_asset(viewtype, imagetype):
|
|
"""Check if this is a base exterior or interior asset."""
|
|
return (viewtype == 'exterior' and imagetype is None) or (viewtype == 'interior' and imagetype is None)
|
|
|
|
def validate_base_asset_format(filename, actual_format, pack_type, viewtype):
|
|
"""
|
|
Validate base asset format with flexible rules:
|
|
- MEC base assets: Strict JPG only (fail if PNG)
|
|
- BAU base assets: JPG preferred, PNG acceptable with warning
|
|
Returns: (is_valid, warning_message)
|
|
"""
|
|
if pack_type == "MEC":
|
|
# MEC base assets must be JPG only
|
|
if actual_format != "JPEG":
|
|
return False, None
|
|
return True, None
|
|
else: # BAU pack
|
|
# BAU base assets: JPG preferred, PNG acceptable with warning
|
|
if actual_format == "JPEG":
|
|
return True, None
|
|
elif actual_format == "PNG":
|
|
return True, f"PNG format found but JPG format is preferred for {viewtype} base assets. Please consider changing this to JPG to match business requirements."
|
|
else:
|
|
return False, None
|
|
|
|
# Use a dictionary to avoid duplicates (keyed by filename)
|
|
failed_images_dict = {}
|
|
warnings_list = []
|
|
|
|
# Track skipped viewtype/imagetype combinations
|
|
skipped_types: Dict[str, Set[Tuple[str, Optional[str]]]] = {"image_format_check": set()}
|
|
|
|
for item in linkingrecord["items"]:
|
|
conditions = item.get("conditions", {})
|
|
viewtype = conditions.get("viewtype")
|
|
imagetype = conditions.get("imagetype")
|
|
|
|
# Skip items with missing viewtype
|
|
if not viewtype:
|
|
logging.debug(f"Skipping item with missing viewtype: {conditions}")
|
|
continue
|
|
|
|
# Check if this is a base asset that needs special handling
|
|
if is_base_asset(viewtype, imagetype):
|
|
# Base assets use flexible validation logic
|
|
expected_format = None # Will be handled in the validation function
|
|
else:
|
|
# Non-base assets use the standard format requirements
|
|
key = (viewtype, imagetype)
|
|
expected_format = None
|
|
|
|
if key in format_requirements:
|
|
expected_format = format_requirements[key]
|
|
# Try with None imagetype as fallback
|
|
elif (viewtype, None) in format_requirements:
|
|
expected_format = format_requirements[(viewtype, None)]
|
|
|
|
# If no requirement is found, record it and skip this item
|
|
if expected_format is None:
|
|
record_skipped_type(skipped_types, "image_format_check", viewtype, imagetype)
|
|
continue
|
|
|
|
records = item.get("records", [])
|
|
for record in records:
|
|
assets = record.get("assets", [])
|
|
for asset in assets:
|
|
filename = asset.get("filename")
|
|
if not filename:
|
|
continue
|
|
|
|
image_path = os.path.join(working_dir, filename)
|
|
if not os.path.exists(image_path):
|
|
# Ignore missing files for this check
|
|
continue
|
|
|
|
# Check actual image format
|
|
# Special handling for AVIF files - use extension validation since PIL may not support them
|
|
if not is_base_asset(viewtype, imagetype) and expected_format == "AVIF":
|
|
# For AVIF files, validate by extension instead of PIL
|
|
if filename.lower().endswith('.avif'):
|
|
# AVIF file with correct extension - pass validation
|
|
continue
|
|
else:
|
|
# AVIF expected but wrong extension
|
|
if filename not in failed_images_dict:
|
|
failed_images_dict[filename] = {
|
|
"filename": filename,
|
|
"viewtype": viewtype,
|
|
"imagetype": imagetype,
|
|
"expected_format": expected_format,
|
|
"actual_format": "Non-AVIF extension"
|
|
}
|
|
continue
|
|
|
|
# Use PIL validation for all other formats
|
|
try:
|
|
with Image.open(image_path) as img:
|
|
actual_format = img.format
|
|
|
|
if is_base_asset(viewtype, imagetype):
|
|
# Use flexible validation for base assets
|
|
is_valid, warning_message = validate_base_asset_format(filename, actual_format, pack_type, viewtype)
|
|
|
|
if not is_valid:
|
|
# Only add an entry if this filename not yet in the dict
|
|
if filename not in failed_images_dict:
|
|
expected_desc = "JPEG" if pack_type == "MEC" else "JPEG (preferred) or PNG (acceptable)"
|
|
failed_images_dict[filename] = {
|
|
"filename": filename,
|
|
"viewtype": viewtype,
|
|
"imagetype": imagetype,
|
|
"expected_format": expected_desc,
|
|
"actual_format": actual_format,
|
|
"pack_type": pack_type
|
|
}
|
|
elif warning_message:
|
|
# Add warning but don't fail
|
|
warnings_list.append({
|
|
"filename": filename,
|
|
"message": warning_message
|
|
})
|
|
else:
|
|
# Standard validation for non-base assets
|
|
# PIL reports "JPEG" for JPG files
|
|
if actual_format != expected_format:
|
|
# Only add an entry if this filename not yet in the dict
|
|
if filename not in failed_images_dict:
|
|
failed_images_dict[filename] = {
|
|
"filename": filename,
|
|
"viewtype": viewtype,
|
|
"imagetype": imagetype,
|
|
"expected_format": expected_format,
|
|
"actual_format": actual_format
|
|
}
|
|
|
|
except Exception as e:
|
|
# If we can't open the image, mark it as failed
|
|
if filename not in failed_images_dict:
|
|
expected_desc = expected_format if expected_format else ("JPEG" if pack_type == "MEC" and is_base_asset(viewtype, imagetype) else "Unknown")
|
|
failed_images_dict[filename] = {
|
|
"filename": filename,
|
|
"viewtype": viewtype,
|
|
"imagetype": imagetype,
|
|
"expected_format": expected_desc,
|
|
"actual_format": f"Error: {str(e)}"
|
|
}
|
|
|
|
# Convert dict of failures to a list
|
|
if failed_images_dict:
|
|
result = {
|
|
"status": "failed",
|
|
"details": {
|
|
"message": "Some images are not in the expected file format.",
|
|
"failed_images": list(failed_images_dict.values()),
|
|
"pack_type": pack_type
|
|
}
|
|
}
|
|
|
|
# Add warnings to failed result if any exist
|
|
if warnings_list:
|
|
result["details"]["warnings"] = warnings_list
|
|
|
|
# Add skipped types to the result if any were found
|
|
if skipped_types["image_format_check"]:
|
|
result["details"]["skipped_types_message"] = f"{len(skipped_types['image_format_check'])} unknown viewtype/imagetype combinations were skipped."
|
|
result["details"]["skipped_types"] = [
|
|
{"viewtype": vt, "imagetype": it if it is not None else "None"}
|
|
for vt, it in skipped_types["image_format_check"]
|
|
]
|
|
|
|
return result
|
|
|
|
# No failures - construct success result
|
|
base_message = "All present images match their required file format."
|
|
|
|
# If we have warnings, it's still a pass but with warnings
|
|
result = {
|
|
"status": "passed",
|
|
"details": {
|
|
"message": base_message,
|
|
"pack_type": pack_type
|
|
}
|
|
}
|
|
|
|
# Add warnings to successful result if any exist
|
|
if warnings_list:
|
|
result["details"]["warnings"] = warnings_list
|
|
result["details"]["message"] = f"{base_message} Some recommendations noted below."
|
|
|
|
# If we skipped any types, add that to the result
|
|
if skipped_types["image_format_check"]:
|
|
result["details"]["skipped_types_message"] = f"{len(skipped_types['image_format_check'])} unknown viewtype/imagetype combinations were skipped."
|
|
result["details"]["skipped_types"] = [
|
|
{"viewtype": vt, "imagetype": it if it is not None else "None"}
|
|
for vt, it in skipped_types["image_format_check"]
|
|
]
|
|
|
|
return result |