import logging
import os
import warnings
import subprocess
import re
from cgl.core.utils.general import cgl_execute
from PIL import Image
from psd_tools import PSDImage
from cgl.apps.ingest.query import file_type
from cgl.core.path.object import PathObject
from cgl.core.path.support import lj_list_dir
from cgl.plugins.alchemy.create import create_folders
from cgl.plugins.alchemy.query import is_numbered_file, get_sequences_from_lj_list_dir
warnings.filterwarnings("ignore", category=UserWarning, module="psd_tools")
logging.getLogger("psd_tools").setLevel(logging.ERROR)
[docs]
def psd_to_image(psd_path):
"""
Converts a PSD file to a PIL Image.
Args:
psd_path (str): Path to the PSD file.
Returns:
PIL.Image: The converted image_plane or None if failed.
"""
try:
psd = PSDImage.open(psd_path)
composite = psd.composite()
return composite.convert("RGB")
except Exception as e:
logging.error(f"Failed to convert PSD to image_plane: {e}")
return None
[docs]
def convert_psd_to_png(psd_path, output_path=None, resolution=None, quiet=True):
"""
Converts a PSD file to a PNG while preserving aspect ratio.
If resolution is provided, resizes the image_plane to fit within it, adding black padding.
Args:
psd_path (str): Path to the PSD file.
output_path (str, optional): Output PNG path. If None, it auto-generates.
resolution (tuple, optional): (width, height) to fit the image_plane within.
If None, keeps the original resolution.
Returns:
str: The output PNG path or None if failed.
"""
if not output_path:
output_path = os.path.splitext(psd_path)[0] + ".png"
img = psd_to_image(psd_path)
if img:
try:
img = resize_and_pad_image(img, resolution) # Apply resizing and padding
img.save(output_path, format="PNG")
if not quiet:
logging.info(f"PNG saved to {output_path} with resolution {img.size}")
return output_path
except Exception as e:
logging.error(f"Failed to save PNG: {e}")
return None
[docs]
def convert_image_to_png(image_path, output_path=None, resolution=None, quiet=True):
"""
Converts any image_plane file (JPG, TIFF, BMP, etc.) to a PNG while preserving aspect ratio.
If resolution is provided, resizes the image_plane to fit within it, adding black padding.
Args:
image_path (str): Path to the source image_plane.
output_path (str, optional): Path to save the PNG file. If None, auto-generates a path.
resolution (tuple, optional): (width, height) to fit the image_plane within.
If None, keeps the original resolution.
Returns:
str: Output PNG file path or None if conversion failed.
"""
if not output_path:
output_path = os.path.splitext(image_path)[0] + ".png"
try:
with Image.open(image_path) as img:
img = img.convert("RGB") # Ensure RGB format
img = resize_and_pad_image(img, resolution) # Apply resizing and padding
img.save(output_path, format="PNG")
if not quiet:
logging.info(f"PNG saved to {output_path} with resolution {img.size}")
return output_path
except Exception as e:
logging.error(f"Failed to convert {image_path} to PNG: {e}")
return None
[docs]
def resize_and_pad_image(img, resolution=None):
"""
Resizes an image_plane while maintaining aspect ratio and pads it with black if needed.
Args:
img (PIL.Image): The input PIL Image object.
resolution (tuple, optional): (width, height) to fit the image_plane within.
If None, returns the original image_plane.
Returns:
PIL.Image: The resized and padded image_plane.
"""
if not resolution:
return img # Keep original resolution
target_width, target_height = resolution
background = Image.new(
"RGB", (target_width, target_height), (0, 0, 0)
) # Black background
# Resize while maintaining aspect ratio
img.thumbnail((target_width, target_height), Image.Resampling.LANCZOS)
# Center the resized image_plane on the black background
paste_x = (target_width - img.width) // 2
paste_y = (target_height - img.height) // 2
background.paste(img, (paste_x, paste_y))
return background # Return final image_plane with correct resolution
[docs]
def convert_to_png(source_path, output_path=None, resolution=None):
"""
Converts any image_plane file (PSD, JPG, TIFF, BMP, etc.) to a full-resolution PNG.
Args:
source_path (str): Path to the source image_plane.
output_path (str, optional): Path to save the PNG file. If None, it auto-generates.
Returns:
str: Output PNG file path or None if conversion failed.
"""
ft = file_type(source_path)
if ft == "image_plane":
return convert_image_to_png(source_path, output_path, resolution=resolution)
elif ft == "psd":
return convert_psd_to_png(source_path, output_path, resolution=resolution)
else:
logging.error(f"Not an image_plane path: {source_path}")
return None
[docs]
def create_png_proxies(path_object, resolution=(1920, 1080)):
"""
This script assumes we are working with files in the "Render" folder that we are seeking to create jpg proxies for
the purpose of creating quicktime movies.
Args:
path_object: The path object to the render folder.
resolution: The resolution to convert the images to.
Returns:
"""
render_folder = path_object.get_render_path(dirname=True)
if "render" not in render_folder.lower():
logging.error(f"Path is not from a render tree: {render_folder}")
return None
render_files = lj_list_dir(render_folder)
if render_files:
proxy_path = path_object.get_hd_proxy_path()
proxy_object = PathObject().from_path_string(proxy_path)
create_folders(proxy_object)
if len(render_files) == 1:
proxy_file = get_proxy_frame(proxy_path, 1)
render_path = str(
os.path.join(render_folder, render_files[0]).replace("\\", "/")
)
convert_to_png(render_path, proxy_file, resolution=resolution)
return proxy_path
for i, render_file in enumerate(render_files):
proxy_file = get_proxy_frame(proxy_path, i + 1)
render_path = str(
os.path.join(render_folder, render_file).replace("\\", "/")
)
# convert_to_png will test the file type if it's an image_plane or psd and convert it to a png
convert_to_png(render_path, proxy_file, resolution=resolution)
return proxy_path
return None
[docs]
def get_proxy_frame(proxy_path, frame_number):
"""
This function will return the path to a specific frame of a jpg proxy.
Args:
proxy_path: Path to the jpg proxy file.
frame_number: The frame number to get the path for.
Returns:
str: The path to the specific frame.
"""
return proxy_path.replace("####", f"{frame_number:04d}")
[docs]
def audio_to_wav(input_audio_path, output_wav_path=None):
"""
Generates an ffmpeg command string to convert an audio file to WAV format.
Args:
input_audio_path (str): Path to the input audio file.
output_wav_path (str, optional): Path to save the output WAV file.
If not provided, it will use the same filename with a .wav extension.
Returns:
str: The formatted ffmpeg command string.
"""
if output_wav_path is None:
base_name = os.path.splitext(input_audio_path)[0]
output_wav_path = f"{base_name}.wav"
command = (
f'ffmpeg -i "{input_audio_path}" -ac 2 -ar 44100 -q:a 0 "{output_wav_path}"'
)
cgl_execute(command)
return output_wav_path
[docs]
def video_to_wav(input_video_path, output_wav_path=None):
"""
Generates an ffmpeg command string to convert a video file to a WAV file.
Args:
input_video_path (str): Path to the input video file.
output_wav_path (str, optional): Path to save the output WAV file. If not provided,
saves in the same directory with the same filename.
Returns:
str: The formatted ffmpeg command string.
"""
if output_wav_path is None:
base_name = os.path.splitext(input_video_path)[0]
output_wav_path = f"{base_name}.wav"
command = f'ffmpeg -i "{input_video_path}" -ac 2 -ar 44100 -q:a 0 -map a "{output_wav_path}"'
cgl_execute(command)
return command
[docs]
def convert_to_wav(input_path, output_wav_path=None):
"""
Converts an audio or video file to WAV format.
Args:
input_path:
output_wav_path:
Returns:
"""
ft = file_type(input_path)
if ft == "audio":
return audio_to_wav(input_path, output_wav_path)
elif ft == "movie":
return video_to_wav(input_path, output_wav_path)
else:
logging.error(f"Not an audio or video file: {input_path}")
return None
[docs]
def convert_png_sequence_to_mp4(
source_sequence,
start_frame=None,
dest_mp4=None,
fps=24,
width=1920,
height=1080,
force=True,
):
"""
Converts a PNG image_plane sequence into an MP4 file.
Args:
source_sequence (str): Path pattern for the PNG sequence (e.g., "frame_####.png").
start_frame (int, optional): First frame number (default: auto-detected).
dest_mp4 (str, optional): Output MP4 file path. Auto-generated if None.
fps (int, optional): Frames per second (default: 24).
width (int, optional): Output width (default: 1920).
height (int, optional): Output height (default: 1080).
force (bool, optional): If True, overwrite existing MP4 (default: True).
Returns:
str: Path to the generated MP4 file or None if conversion failed.
"""
print(source_sequence)
# ✅ Convert .####. to .%04d.
source_sequence = re.sub(
r"\.#+\.", lambda m: f".%0{len(m.group())-2}d.", source_sequence
)
if not start_frame:
start_frame = 1 # Default to frame 1 if not provided
if not dest_mp4:
dest_mp4 = os.path.splitext(source_sequence)[0] + ".mp4"
if os.path.exists(dest_mp4) and not force:
logging.info(f"MP4 already exists at {dest_mp4}, skipping conversion.")
return dest_mp4
ffmpeg_cmd = (
f'ffmpeg -start_number {start_frame} -framerate {fps} -i "{source_sequence}" '
f'-s {width}x{height} -c:v libx264 -crf 18 -pix_fmt yuv420p "{dest_mp4}"'
)
os.makedirs(os.path.dirname(dest_mp4), exist_ok=True)
print(f"Running: {ffmpeg_cmd}")
result = subprocess.run(ffmpeg_cmd, shell=True, capture_output=True, text=True)
if result.returncode == 0:
logging.info(f"MP4 successfully created: {dest_mp4}")
return dest_mp4
else:
logging.error(f"FFmpeg error: {result.stderr}")
return None
[docs]
def create_mp4_from_proxy(proxy_folder_path):
"""
Converts a proxy sequence into an mp4 preview file.
Args:
proxy_folder_path:
Returns:
"""
if not os.path.isdir(proxy_folder_path):
proxy_folder_path = os.path.dirname(proxy_folder_path)
po = PathObject().from_path_string(proxy_folder_path)
mp4_path = po.get_preview_path(ext=".mp4")
if os.path.exists(mp4_path):
os.remove(mp4_path)
dir_contents = lj_list_dir(proxy_folder_path)
sequences = get_sequences_from_lj_list_dir(dir_contents)
if not sequences:
for each in dir_contents:
if is_numbered_file(each):
print("Found single frame for mp4 conversion", each)
# replace the frame number with ####
frame_number_rgex = re.compile(r"\d{4}")
each = frame_number_rgex.sub("####", each)
file_path = os.path.join(proxy_folder_path, each).replace("\\", "/")
return create_mp4_from_single_proxy(file_path)
sequence, frange = sequences[0].split(" ")
sequence_path = str(os.path.join(proxy_folder_path, sequence).replace("\\", "/"))
convert_png_sequence_to_mp4(sequence_path, dest_mp4=mp4_path)
print(f"MP4 created at {mp4_path}")
return mp4_path
[docs]
def create_mp4_from_single_proxy(proxy_path):
"""
Converts a single proxy image_plane into an mp4 preview file.
Args:
proxy_path:
Returns:
"""
po = PathObject().from_path_string(proxy_path)
mp4_path = po.get_preview_path(ext=".mp4")
if os.path.exists(mp4_path):
os.remove(mp4_path)
convert_png_sequence_to_mp4(proxy_path, dest_mp4=mp4_path)
logging.info(f"MP4 created at {mp4_path}")
return mp4_path
if __name__ == "__main__":
path_ = r"E:\Alchemy\CGL\ARC\VERSIONS\source\shots\intro\SEQ\snd\default\tom\004.001\high\ARC_Ep_01_Pt_01_Shot_04Camera.mp4"
# path_object = PathObject().from_path_string(path_)
convert_to_wav(path_)