From 8a560af1dc6120ca21717f0ad59915eb2d067e2b Mon Sep 17 00:00:00 2001 From: Leya Wehner Date: Thu, 17 Sep 2026 21:58:24 +0200 Subject: [PATCH 1/3] Add Python scripts for DCP and linear profile automation --- .gitignore | 13 +++ README.md | 59 ++++++++++++- create_linear.py | 182 +++++++++++++++++++++++++++++++++++++++ create_profiles.py | 206 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 456 insertions(+), 4 deletions(-) create mode 100644 .gitignore create mode 100644 create_linear.py create mode 100644 create_profiles.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..71da297 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Generated Profiles & Build Output +output/ + +# Compiled files & caches +__pycache__/ +*.pyc + +# Local dcpTool binaries +dcptool.exe +dcptool +iconv.dll +libxml2.dll +zlib1.dll diff --git a/README.md b/README.md index c191b8a..05c1285 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,15 @@ The Film Looks are: * Nostalgic Neg (lut only) * Bleach Bypass (lut only) -## Update: Nov 22 2025. Improved luts +## Updates + +### Sep 2026: Automation scripts +Added interactive Python scripts (`create_profiles.py` and `create_linear.py`) to streamline camera profile generation and deployment. +* Automatically locates your camera's `Adobe Standard.dcp` profile. +* Batch compiles all native Fujifilm DCP profiles with correct tone curves and metadata. +* Creates linear base profiles required for working with 3D LUTs in Camera Raw. + +### Nov 22 2025: Improved luts The luts now have improved accuracy and smoothness, especially for bright or saturated colors. dcp profiles have increased precision and are now a more accurate match to the lut. I have found that there is still a small discrepancy in how adobe applies the tone curve. This only affects some very bright and saturated colors. @@ -32,7 +40,49 @@ Included for each profile are: For more details on editing profiles and making a linear profile for use with the luts, see my blog post [Making Linear Camera Profiles with dcpTool](https://abpy.github.io/2023/05/20/linear-profiles.html) -The cube LUTs are intended to be applied to an image with linear contrast. A linear camera profile is required for a correct result. See below for details +The cube LUTs are intended to be applied to an image with linear contrast. A linear camera profile is required for a correct result. See below for details. + +--- + +## Automation Scripts (Python) + +Two interactive Python scripts are provided to automate profile generation and installation: + +### Prerequisites +* **Python 3.8+** +* **[dcpTool](https://dcptool.sourceforge.net/)**: Recommended to add to your system `PATH`. If placed directly in the repository root directory instead, make sure to copy all accompanying `.dll` files alongside `dcptool.exe` into the folder (on Windows). +* **Adobe Lightroom Classic or Adobe Photoshop**: An active installation providing the base camera profiles (`Adobe Standard`). + +--- + +### 1. Batch Generate Native DCP Profiles (`create_profiles.py`) + +Automates batch-building all available Fujifilm DCP profiles for your specific camera model. + +```bash +python create_profiles.py +``` + +* **Interactive Selection**: Prompts for your camera model (e.g. `d5300` or `Nikon D5300`) and locates the matching `Adobe Standard.dcp` automatically. +* **Batch Compilation**: Decompiles the base profile, strips existing LookTables, applies ToneCurves from `xml tables/`, sets metadata tags (`DefaultBlackRender=1`, `ProfileLookTableEncoding=1`), names profiles cleanly (e.g., `Fuji Classic Chrome`), and outputs compiled `.dcp` files into `output/`. +* **One-Click Install**: Prompts to directly copy the generated profiles into your system CameraRaw profile directory. + +--- + +### 2. Generate Linear Base Profile (`create_linear.py`) + +Creates the camera-specific linear base profile (` Linear.dcp`) required for applying 3D LUTs in Adobe Camera Raw. + +```bash +python create_linear.py +``` + +* **Linearization**: Strips original LookTables from your camera's `Adobe Standard.dcp`, inserts an exact 1:1 diagonal ToneCurve, sets `DefaultBlackRender=1`, and compiles the profile. +* **One-Click Install**: Prompts to copy the linear profile directly into `CameraProfiles/Linear Profiles` for immediate use in Camera Raw. + +--- + +## Manual Workflow #### Conversion luts for Classic Neg, Bleach Bypass, and Nostalgic Neg These luts will convert images processed with Provia to the classic neg, bleach bypass, or nostalgic neg film simulations. They can be used with non fuji cameras using the the dng tables provided here, or with fuji x-trans cameras that don't come with these profiles using the Provia camera matching profile. They can also be applied directly to camera jpegs. The .cube files can be found in `provia conversion luts/` and are provided in both DisplayP3 and sRGB. @@ -51,8 +101,10 @@ To make a profile for your camera: * change `` * convert back to dcp +*(Note: This process can be automated using `create_profiles.py`.)* + #### Using the cube luts in CameraRaw -You will need a [linear camera profile](https://abpy.github.io/2023/05/20/linear-profiles.html) +You will need a [linear camera profile](https://abpy.github.io/2023/05/20/linear-profiles.html) (create one via `create_linear.py` or manually): * Open an image with default settings (everything at 0, white balance: 'As Shot') * Select the linear camera profile * Option/Alt click the new preset button @@ -72,4 +124,3 @@ This means you may modify, make derivatives, or distribute them, eg; for another All Photographs produced with these profiles are entirely your own work, and not derivatives. The licence applies only to the profiles. The Trademarks "Fujifilm", "Provia", "Velvia", "Astia", and "Adobe" are used for identification purposes only. No software from Fujifilm or Adobe are contained in this repository except for the included adobe standard camera profiles. - diff --git a/create_linear.py b/create_linear.py new file mode 100644 index 0000000..59521ab --- /dev/null +++ b/create_linear.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +""" +Linear Camera Profile Generator +Creates a camera-specific linear DCP profile required for applying 3D LUTs in Camera Raw. +""" + +import os +import sys +import platform +import shutil +import subprocess +import re +import tempfile +from pathlib import Path + +def get_adobe_paths(): + system = platform.system() + if system == "Windows": + base_search = Path(os.environ.get("ProgramData", "C:\\ProgramData")) / "Adobe" / "CameraRaw" / "CameraProfiles" / "Adobe Standard" + user_profiles_dir = Path(os.environ.get("APPDATA", "")) / "Adobe" / "CameraRaw" / "CameraProfiles" + elif system == "Darwin": + base_search = Path("/Library/Application Support/Adobe/CameraRaw/CameraProfiles/Adobe Standard") + user_profiles_dir = Path.home() / "Library" / "Application Support" / "Adobe" / "CameraRaw" / "CameraProfiles" + else: + base_search = Path.cwd() + user_profiles_dir = Path.cwd() / "installed_dcp" + return base_search, user_profiles_dir + +def find_dcptool(): + candidates = ["dcptool.exe", "dcpTools.exe", "dcptool", "dcpTool"] + for name in candidates: + local_path = Path.cwd() / name + if local_path.is_file(): + return str(local_path.resolve()) + for name in candidates: + system_path = shutil.which(name) + if system_path: + return system_path + return None + +def search_camera_profile(query, search_dir): + candidates = [] + if search_dir.is_dir(): + candidates.extend(list(search_dir.glob("*.dcp"))) + candidates.extend(list(Path.cwd().glob("*.dcp"))) + + unique_candidates = list({p.resolve(): p for p in candidates}.values()) + query_lower = query.lower() + matches = [p for p in unique_candidates if query_lower in p.name.lower()] + return sorted(matches, key=lambda x: x.name) + +def ask_yes_no(prompt_text, default=True): + hint = "[Y/n]" if default else "[y/N]" + user_input = input(f"{prompt_text} {hint}: ").strip().lower() + if not user_input: + return default + return user_input in ["y", "yes", "j", "ja"] + +def select_profile_interactive(adobe_search_dir): + while True: + camera_query = input("\nEnter camera model (e.g., 'd5300' or 'Nikon D5300') [or 'q' to quit]: ").strip() + if not camera_query or camera_query.lower() in ["q", "quit", "exit"]: + sys.exit("Aborted by user.") + + matches = search_camera_profile(camera_query, adobe_search_dir) + if not matches: + print(f"No profile matching '{camera_query}' found.") + if ask_yes_no("Do you want to search again?", default=True): + continue + sys.exit("Aborted by user.") + + print(f"\nFound profile(s) matching '{camera_query}':") + for idx, match in enumerate(matches, 1): + print(f" [{idx}] {match.name} ({match.parent})") + print(" [s] Search again") + print(" [q] Quit") + + while True: + choice = input(f"\nSelect an option (1-{len(matches)}, 's', 'q'): ").strip().lower() + if choice in ["q", "quit", "exit"]: + sys.exit("Aborted by user.") + if choice in ["s", "search"]: + break + try: + idx = int(choice) + if 1 <= idx <= len(matches): + return matches[idx - 1] + except ValueError: + pass + print(f"Invalid input. Please enter 1-{len(matches)}, 's' to search again, or 'q' to quit.") + +def set_xml_tag(xml_text, tag, value): + pattern = rf"<{tag}>.*?" + if re.search(pattern, xml_text): + return re.sub(pattern, f"<{tag}>{value}", xml_text) + match = re.search(r"\s*$", xml_text) + if match: + idx = match.start() + return xml_text[:idx] + f" <{tag}>{value}\n" + xml_text[idx:] + return xml_text + f"\n<{tag}>{value}" + +def main(): + print("=== Linear Camera Profile Generator ===") + + dcptool_bin = find_dcptool() + if not dcptool_bin: + sys.exit("Error: 'dcpTool' binary not found in working directory or system PATH.") + + adobe_search_dir, user_profiles_dir = get_adobe_paths() + selected_dcp = select_profile_interactive(adobe_search_dir) + + camera_name = selected_dcp.stem.replace(" Adobe Standard", "").strip() + profile_display_name = "Adobe Standard Linear" + + out_dir = Path.cwd() / "output" / f"{camera_name} Linear" + out_dir.mkdir(parents=True, exist_ok=True) + target_dcp = out_dir / f"{camera_name} Linear.dcp" + + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + temp_base_dcp = temp_dir / selected_dcp.name + temp_base_xml = temp_dir / f"{selected_dcp.stem}.xml" + shutil.copy2(selected_dcp, temp_base_dcp) + + print(f"\nDecompiling '{selected_dcp.name}'...") + res = subprocess.run([dcptool_bin, "-d", str(temp_base_dcp), str(temp_base_xml)], capture_output=True, text=True) + if res.returncode != 0 or not temp_base_xml.is_file(): + err_msg = (res.stderr or res.stdout).strip() + sys.exit(f"dcpTool decompile failed: {err_msg if err_msg else 'Unknown error'}") + + with open(temp_base_xml, "r", encoding="utf-8", errors="ignore") as f: + xml_content = f.read() + + # 1. Remove LookTable + xml_content = re.sub(r"", "", xml_content) + + # 2. Set ToneCurve to strictly linear (0 to 1 diagonal) + linear_curve = ( + ' \n' + ' \n' + ' \n' + ' ' + ) + if re.search(r"", xml_content): + xml_content = re.sub(r"", linear_curve, xml_content, count=1) + else: + match = re.search(r"\s*$", xml_content) + idx = match.start() + xml_content = xml_content[:idx] + linear_curve + "\n" + xml_content[idx:] + + # 3. Set ProfileName + xml_content = set_xml_tag(xml_content, "ProfileName", profile_display_name) + + # 4. Set DefaultBlackRender + xml_content = set_xml_tag(xml_content, "DefaultBlackRender", "1") + + target_xml = temp_dir / f"{camera_name} Linear.xml" + with open(target_xml, "w", encoding="utf-8") as f: + f.write(xml_content) + + print(f"Compiling '{target_dcp.name}'...") + compile_res = subprocess.run([dcptool_bin, "-c", str(target_xml), str(target_dcp)], capture_output=True, text=True) + + if compile_res.returncode != 0 or not target_dcp.is_file(): + err_msg = (compile_res.stderr or compile_res.stdout).strip() + sys.exit(f"Compilation failed: {err_msg if err_msg else 'Unknown error'}") + + print(f"[OK] Linear profile created at:\n{target_dcp}") + + # Installation + print("\n--- Deployment ---") + if ask_yes_no("Install linear profile directly into CameraRaw?", default=True): + target_install_dir = user_profiles_dir / "Linear Profiles" + target_install_dir.mkdir(parents=True, exist_ok=True) + shutil.copy2(target_dcp, target_install_dir / target_dcp.name) + print(f"\nSuccessfully copied to:\n{target_install_dir / target_dcp.name}") + print("\nRestart Photoshop / Camera Raw to use this profile as your base for 3D LUTs.") + else: + print(f"\nManual installation: Copy '{target_dcp.name}' to:\n{user_profiles_dir}") + +if __name__ == "__main__": + main() diff --git a/create_profiles.py b/create_profiles.py new file mode 100644 index 0000000..24adb81 --- /dev/null +++ b/create_profiles.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 +""" +Fujifilm DCP Camera Profile Generator +Compiles native DCP profiles directly for Adobe Lightroom & Camera Raw. +""" + +import os +import sys +import platform +import shutil +import subprocess +import re +import tempfile +from pathlib import Path + +def get_adobe_paths(): + """Detect default Adobe CameraRaw profile paths across operating systems.""" + system = platform.system() + if system == "Windows": + base_search = Path(os.environ.get("ProgramData", "C:\\ProgramData")) / "Adobe" / "CameraRaw" / "CameraProfiles" / "Adobe Standard" + user_profiles = Path(os.environ.get("APPDATA", "")) / "Adobe" / "CameraRaw" / "CameraProfiles" + elif system == "Darwin": # macOS + base_search = Path("/Library/Application Support/Adobe/CameraRaw/CameraProfiles/Adobe Standard") + user_profiles = Path.home() / "Library" / "Application Support" / "Adobe" / "CameraRaw" / "CameraProfiles" + else: + base_search = Path.cwd() + user_profiles = Path.cwd() / "installed_profiles" + return base_search, user_profiles + +def find_dcptool(): + """Locate the dcpTool binary in the current working directory or system PATH.""" + candidates = ["dcptool.exe", "dcpTools.exe", "dcptool", "dcpTool"] + for name in candidates: + local_path = Path.cwd() / name + if local_path.is_file(): + return str(local_path.resolve()) + for name in candidates: + system_path = shutil.which(name) + if system_path: + return system_path + return None + +def search_camera_profile(query, search_dir): + """Search for matching DCP profiles in Adobe Standard directory and current working directory.""" + candidates = [] + if search_dir.is_dir(): + candidates.extend(list(search_dir.glob("*.dcp"))) + candidates.extend(list(Path.cwd().glob("*.dcp"))) + + unique_candidates = list({p.resolve(): p for p in candidates}.values()) + query_lower = query.lower() + matches = [p for p in unique_candidates if query_lower in p.name.lower()] + return sorted(matches, key=lambda x: x.name) + +def ask_yes_no(prompt_text, default=True): + """Prompt user for a yes/no decision with default support.""" + hint = "[Y/n]" if default else "[y/N]" + user_input = input(f"{prompt_text} {hint}: ").strip().lower() + if not user_input: + return default + return user_input in ["y", "yes"] + +def select_profile_interactive(adobe_search_dir): + """Interactive loop to query, select, re-search, or abort base profile selection.""" + while True: + camera_query = input("\nEnter camera model (e.g., 'd5300' or 'Nikon D5300') [or 'q' to quit]: ").strip() + if not camera_query or camera_query.lower() in ["q", "quit", "exit"]: + sys.exit("Aborted by user.") + + matches = search_camera_profile(camera_query, adobe_search_dir) + if not matches: + print(f"No profile matching '{camera_query}' found.") + if ask_yes_no("Do you want to search again?", default=True): + continue + sys.exit("Aborted by user.") + + print(f"\nFound profile(s) matching '{camera_query}':") + for idx, match in enumerate(matches, 1): + print(f" [{idx}] {match.name} ({match.parent})") + print(" [s] Search again") + print(" [q] Quit") + + while True: + choice = input(f"\nSelect an option (1-{len(matches)}, 's', 'q'): ").strip().lower() + if choice in ["q", "quit", "exit"]: + sys.exit("Aborted by user.") + if choice in ["s", "search"]: + break + try: + idx = int(choice) + if 1 <= idx <= len(matches): + return matches[idx - 1] + except ValueError: + pass + print(f"Invalid input. Please enter 1-{len(matches)}, 's' to search again, or 'q' to quit.") + +def set_xml_tag(xml_text, tag, value): + """Update an existing XML tag or insert it before the closing root tag.""" + pattern = rf"<{tag}>.*?" + if re.search(pattern, xml_text): + return re.sub(pattern, f"<{tag}>{value}", xml_text) + match = re.search(r"\s*$", xml_text) + if match: + idx = match.start() + return xml_text[:idx] + f" <{tag}>{value}\n" + xml_text[idx:] + return xml_text + f"\n<{tag}>{value}" + +def clean_look_name(stem_name): + """Format the file stem cleanly and strip duplicate fuji prefixes.""" + name = stem_name.replace("_", " ").strip() + name = re.sub(r"(?i)^fuji(film)?\s*", "", name) + return name.title() + +def main(): + print("=== Fujifilm Camera Profile Generator ===") + + dcptool_bin = find_dcptool() + if not dcptool_bin: + sys.exit("Error: 'dcpTool' binary not found in working directory or system PATH.") + + tables_dir = Path.cwd() / "xml tables" + if not tables_dir.is_dir(): + sys.exit(f"Error: Directory '{tables_dir.name}' not found in current working directory.") + + txt_files = sorted(list(tables_dir.glob("*.txt")), key=lambda x: x.name) + if not txt_files: + sys.exit(f"Error: No .txt files found in '{tables_dir.name}' directory.") + + adobe_search_dir, user_profiles_dir = get_adobe_paths() + selected_dcp = select_profile_interactive(adobe_search_dir) + + camera_name = selected_dcp.stem.replace(" Adobe Standard", "").strip() + + out_dir = Path.cwd() / "output" / f"Fujifilm Simulations {camera_name}" + out_dir.mkdir(parents=True, exist_ok=True) + + with tempfile.TemporaryDirectory() as temp_dir_str: + temp_dir = Path(temp_dir_str) + temp_base_dcp = temp_dir / selected_dcp.name + temp_base_xml = temp_dir / f"{selected_dcp.stem}.xml" + shutil.copy2(selected_dcp, temp_base_dcp) + + print(f"\nDecompiling base profile '{selected_dcp.name}'...") + res = subprocess.run([dcptool_bin, "-d", str(temp_base_dcp), str(temp_base_xml)], capture_output=True, text=True) + if res.returncode != 0 or not temp_base_xml.is_file(): + err_msg = (res.stderr or res.stdout).strip() + sys.exit(f"dcpTool decompile failed: {err_msg if err_msg else 'Unknown error'}") + + with open(temp_base_xml, "r", encoding="utf-8", errors="ignore") as f: + base_xml_content = f.read() + + cleaned_base_xml = re.sub(r"", "", base_xml_content) + + print(f"\nBuilding profiles for {camera_name}:") + for txt_file in txt_files: + look_name = clean_look_name(txt_file.stem) + + # In Lightroom UI: "Fuji " (e.g. "Fuji Astia", "Fuji Classic Chrome") + display_name = f"Fuji {look_name}" + + # Descriptive filename on disk + target_dcp = out_dir / f"{camera_name} - {look_name}.dcp" + + with open(txt_file, "r", encoding="utf-8", errors="ignore") as f: + replacement_snippet = f.read().strip() + + if re.search(r"", cleaned_base_xml): + modified_xml = re.sub(r"", replacement_snippet, cleaned_base_xml, count=1) + else: + match = re.search(r"\s*$", cleaned_base_xml) + idx = match.start() + modified_xml = cleaned_base_xml[:idx] + replacement_snippet + "\n" + cleaned_base_xml[idx:] + + # Apply required metadata tags[cite: 1] + modified_xml = set_xml_tag(modified_xml, "DefaultBlackRender", "1") + modified_xml = set_xml_tag(modified_xml, "ProfileLookTableEncoding", "1") + modified_xml = set_xml_tag(modified_xml, "ProfileName", display_name) + + target_xml = temp_dir / f"{look_name}.xml" + with open(target_xml, "w", encoding="utf-8") as f: + f.write(modified_xml) + + compile_res = subprocess.run([dcptool_bin, "-c", str(target_xml), str(target_dcp)], capture_output=True, text=True) + if compile_res.returncode == 0 and target_dcp.is_file(): + print(f" [OK] '{display_name}' -> {target_dcp.name}") + else: + err_msg = (compile_res.stderr or compile_res.stdout).strip() + print(f" [FAILED] {target_dcp.name}: {err_msg if err_msg else 'Unknown error'}") + + print(f"\nBuild complete. Profiles saved to: {out_dir}") + + # Installation Prompt + print("\n--- Deployment ---") + if ask_yes_no("Do you want to install these profiles directly to Lightroom/CameraRaw?", default=True): + target_install_dir = user_profiles_dir / f"Fujifilm Simulations {camera_name}" + target_install_dir.mkdir(parents=True, exist_ok=True) + for dcp in out_dir.glob("*.dcp"): + shutil.copy2(dcp, target_install_dir / dcp.name) + + print(f"\nSuccessfully installed to:\n{target_install_dir}") + print("\nRestart Lightroom Classic to load the profiles.") + else: + print(f"\nManual installation: Copy the folder '{out_dir.name}' to:\n{user_profiles_dir}") + +if __name__ == "__main__": + main() From 070deabc96814ef62fc6f1cdf380b4bf04ec8897 Mon Sep 17 00:00:00 2001 From: Leya Wehner Date: Thu, 17 Sep 2026 22:30:01 +0200 Subject: [PATCH 2/3] fix: restrict profile search to Adobe Standard and add SPDX license headers --- create_linear.py | 8 +++++++- create_profiles.py | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/create_linear.py b/create_linear.py index 59521ab..b34194c 100644 --- a/create_linear.py +++ b/create_linear.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +# SPDX-License-Identifier: MIT """ Linear Camera Profile Generator Creates a camera-specific linear DCP profile required for applying 3D LUTs in Camera Raw. +Licensed under the MIT License. """ import os @@ -46,7 +48,11 @@ def search_camera_profile(query, search_dir): unique_candidates = list({p.resolve(): p for p in candidates}.values()) query_lower = query.lower() - matches = [p for p in unique_candidates if query_lower in p.name.lower()] + matches = [ + p for p in unique_candidates + if query_lower in p.name.lower() + and (p.stem.lower() == "adobe standard" or p.stem.lower().endswith(" adobe standard")) + ] return sorted(matches, key=lambda x: x.name) def ask_yes_no(prompt_text, default=True): diff --git a/create_profiles.py b/create_profiles.py index 24adb81..85e2c13 100644 --- a/create_profiles.py +++ b/create_profiles.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 +# SPDX-License-Identifier: MIT """ Fujifilm DCP Camera Profile Generator Compiles native DCP profiles directly for Adobe Lightroom & Camera Raw. +Licensed under the MIT License. """ import os @@ -49,7 +51,11 @@ def search_camera_profile(query, search_dir): unique_candidates = list({p.resolve(): p for p in candidates}.values()) query_lower = query.lower() - matches = [p for p in unique_candidates if query_lower in p.name.lower()] + matches = [ + p for p in unique_candidates + if query_lower in p.name.lower() + and (p.stem.lower() == "adobe standard" or p.stem.lower().endswith(" adobe standard")) + ] return sorted(matches, key=lambda x: x.name) def ask_yes_no(prompt_text, default=True): From 4a18a4144b457820ae89dce5bb6b02b9792ba253 Mon Sep 17 00:00:00 2001 From: Leya Wehner Date: Thu, 17 Sep 2026 23:18:49 +0200 Subject: [PATCH 3/3] fix(profiles): track build outputs and prevent installing stale DCPs --- create_linear.py | 2 +- create_profiles.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/create_linear.py b/create_linear.py index b34194c..cd69fad 100644 --- a/create_linear.py +++ b/create_linear.py @@ -25,7 +25,7 @@ def get_adobe_paths(): user_profiles_dir = Path.home() / "Library" / "Application Support" / "Adobe" / "CameraRaw" / "CameraProfiles" else: base_search = Path.cwd() - user_profiles_dir = Path.cwd() / "installed_dcp" + user_profiles_dir = Path.cwd() / "installed_profiles" return base_search, user_profiles_dir def find_dcptool(): diff --git a/create_profiles.py b/create_profiles.py index 85e2c13..7ec0efa 100644 --- a/create_profiles.py +++ b/create_profiles.py @@ -157,6 +157,9 @@ def main(): cleaned_base_xml = re.sub(r"", "", base_xml_content) + successful_dcps = [] + failed_dcps = [] + print(f"\nBuilding profiles for {camera_name}:") for txt_file in txt_files: look_name = clean_look_name(txt_file.stem) @@ -177,7 +180,7 @@ def main(): idx = match.start() modified_xml = cleaned_base_xml[:idx] + replacement_snippet + "\n" + cleaned_base_xml[idx:] - # Apply required metadata tags[cite: 1] + # Apply required metadata tags modified_xml = set_xml_tag(modified_xml, "DefaultBlackRender", "1") modified_xml = set_xml_tag(modified_xml, "ProfileLookTableEncoding", "1") modified_xml = set_xml_tag(modified_xml, "ProfileName", display_name) @@ -189,21 +192,29 @@ def main(): compile_res = subprocess.run([dcptool_bin, "-c", str(target_xml), str(target_dcp)], capture_output=True, text=True) if compile_res.returncode == 0 and target_dcp.is_file(): print(f" [OK] '{display_name}' -> {target_dcp.name}") + successful_dcps.append(target_dcp) else: err_msg = (compile_res.stderr or compile_res.stdout).strip() print(f" [FAILED] {target_dcp.name}: {err_msg if err_msg else 'Unknown error'}") + failed_dcps.append((display_name, err_msg)) + + if not successful_dcps: + sys.exit("\nError: No profiles could be compiled. Aborting.") - print(f"\nBuild complete. Profiles saved to: {out_dir}") + if failed_dcps: + print(f"\nBuild finished with {len(failed_dcps)} failure(s). {len(successful_dcps)} profile(s) saved to: {out_dir}") + else: + print(f"\nBuild complete. All profiles saved to: {out_dir}") # Installation Prompt print("\n--- Deployment ---") if ask_yes_no("Do you want to install these profiles directly to Lightroom/CameraRaw?", default=True): target_install_dir = user_profiles_dir / f"Fujifilm Simulations {camera_name}" target_install_dir.mkdir(parents=True, exist_ok=True) - for dcp in out_dir.glob("*.dcp"): + for dcp in successful_dcps: shutil.copy2(dcp, target_install_dir / dcp.name) - print(f"\nSuccessfully installed to:\n{target_install_dir}") + print(f"\nSuccessfully installed {len(successful_dcps)} profile(s) to:\n{target_install_dir}") print("\nRestart Lightroom Classic to load the profiles.") else: print(f"\nManual installation: Copy the folder '{out_dir.name}' to:\n{user_profiles_dir}")