Source code for cgl.core.convert

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_)