January 22, 2026

User Guide

This tool allows you to batch-process RAW images using the darktable-cli command via a simple web interface. It is useful for benchmarking or debugging, as it captures and displays detailed logs.

Copy the code into your text editor and save the file e.g. as dt-logs.py

Python
import datetime
import os
import subprocess
import streamlit as st

# --- PATH CONFIGURATION ---
# Path to your images (RAW + XMP)
IMAGE_DIR = "/path/to/your/images/"
# Path to the output folder
OUTPUT_DIR = "/path/to/your/images/export/"
# Command (if not in PATH, provide full path here)
DARKTABLE_CLI = "darktable-cli"

# --- COLOR CONFIGURATION (CSS) ---
# You can change the colors here!
COLOR_PRIMARY = "#007BFF"       # Main color (e.g., Button, Checkboxes) - Currently: Blue
COLOR_BUTTON_TEXT = "#FFFFFF"   # Text color on the button
COLOR_BG_SECONDARY = "#262730"  # Background color for input fields/tags

# Supported RAW extensions
RAW_EXTENSIONS = (
    ".ARW", ".CR2", ".CR3", ".NEF", ".DNG", ".ORF",
    ".RAF", ".PEF", ".RW2", ".SRW", ".X3F", ".GPR",
)

# Ensure output directory exists
os.makedirs(OUTPUT_DIR, exist_ok=True)

st.set_page_config(page_title="Darktable Web Runner", layout="wide")

# --- CSS INJECTION ---
# This block overrides the default design with your variables
st.markdown(f"""
<style>
    /* Change Main Button Color */
    div.stButton > button:first-child {{
        background-color: {COLOR_PRIMARY} !important;
        color: {COLOR_BUTTON_TEXT} !important;
        border-color: {COLOR_PRIMARY} !important;
    }}
    div.stButton > button:first-child:hover {{
        background-color: {COLOR_PRIMARY}99 !important; /* Slightly transparent on hover */
        border-color: {COLOR_BUTTON_TEXT} !important;
    }}
    
    /* Checkbox Color */
    div.stCheckbox > label > div[role="checkbox"][aria-checked="true"] {{
        background-color: {COLOR_PRIMARY} !important;
    }}

    /* Multiselect Tags Color */
    .stMultiSelect span[data-baseweb="tag"] {{
        background-color: {COLOR_BG_SECONDARY} !important;
        border: 1px solid {COLOR_PRIMARY} !important;
    }}
    
    /* Progress Bar Color */
    .stProgress > div > div > div > div {{
        background-color: {COLOR_PRIMARY} !important;
    }}
    
    /* Focus Border for Input Fields */
    input:focus {{
        border-color: {COLOR_PRIMARY} !important;
        box-shadow: 0 0 0 1px {COLOR_PRIMARY} !important;
    }}
</style>
""", unsafe_allow_html=True)

st.title("Darktable CLI Web Interface")

# 1. List Files
try:
    all_files = os.listdir(IMAGE_DIR)
    # Filter files ending with RAW extensions (case-insensitive)
    raw_files = [f for f in all_files if f.upper().endswith(RAW_EXTENSIONS)]
    raw_files.sort()
except FileNotFoundError:
    st.error(f"The folder {IMAGE_DIR} was not found.")
    raw_files = []

if not raw_files:
    st.warning(f"No RAW files found in {IMAGE_DIR}.")
else:
    # --- UI AREA ---

    # 1. File Selection (Full Width)
    selected_files = st.multiselect(
        f"Select images ({len(raw_files)} found):", raw_files
    )

    st.write("---")  # Separator

    # 2. Settings (All in one row with columns)
    # We split the width: 3 parts for name, 1 part each for checkboxes
    col_name, col_c1, col_c2, col_c3, col_c4 = st.columns([3, 1, 1, 1, 1])

    with col_name:
        default_log_name = (
            f"dt_log_{datetime.datetime.now().strftime('%Y%m%d_%H%M')}.txt"
        )
        log_filename = st.text_input(
            "Log Filename:",
            value=default_log_name,
            label_visibility="collapsed",
            placeholder="Log Filename",
        )

    # Checkboxes
    with col_c1:
        use_opencl = st.checkbox("-d opencl", value=True)
    with col_c2:
        use_tiling = st.checkbox("-d tiling", value=True)
    with col_c3:
        use_perf = st.checkbox("-d perf", value=True)
    with col_c4:
        use_pipe = st.checkbox("-d pipe", value=True)

    st.write("---")  # Separator

    # Start Button
    if st.button("Start Processing", type="primary") and selected_files:
        progress_bar = st.progress(0)
        status_text = st.empty()

        # Container for Logs (Expander)
        # expanded=False means: collapsed by default
        with st.expander(
            "Show detailed logs (Click to expand)", expanded=False
        ):
            log_area = st.empty()

        all_logs = f"--- START SESSION: {datetime.datetime.now()} ---\n"
        all_logs += f"Selected Files: {selected_files}\n\n"

        for i, filename in enumerate(selected_files):
            input_path = os.path.join(IMAGE_DIR, filename)
            xmp_path = input_path + ".xmp"

            output_filename = os.path.splitext(filename)[0] + "_test.jpg"
            output_path = os.path.join(OUTPUT_DIR, output_filename)

            cmd = [DARKTABLE_CLI, input_path, xmp_path, output_path, "--core"]

            if use_opencl:
                cmd.extend(["-d", "opencl"])
            if use_tiling:
                cmd.extend(["-d", "tiling"])
            if use_perf:
                cmd.extend(["-d", "perf"])
            if use_pipe:
                cmd.extend(["-d", "pipe"])

            status_text.text(
                f"Processing ({i + 1}/{len(selected_files)}): {filename}..."
            )

            try:
                result = subprocess.run(
                    cmd, capture_output=True, text=True, check=False
                )

                log_entry = f"=== FILE: {filename} ===\n"
                log_entry += f"CMD: {' '.join(cmd)}\n"
                log_entry += "--- STDOUT ---\n" + result.stdout + "\n"
                log_entry += "--- STDERR ---\n" + result.stderr + "\n"
                log_entry += f"=== END FILE (Exit: {result.returncode}) ===\n\n"

                all_logs += log_entry

                # Update inside the expander
                log_area.code(all_logs, language="bash")

            except Exception as e:
                err_msg = f"SYSTEM ERROR at {filename}: {str(e)}\n"
                st.error(err_msg)
                all_logs += err_msg

            progress_bar.progress((i + 1) / len(selected_files))

        status_text.success("All tasks completed.")

        # Save Log
        if log_filename:
            if not log_filename.endswith(".txt"):
                log_filename += ".txt"
            log_path = os.path.join(OUTPUT_DIR, log_filename)
            try:
                with open(log_path, "w", encoding="utf-8") as f:
                    f.write(all_logs)
                st.success(f"Log saved at: {log_path}")
            except Exception as e:
                st.error(f"Error saving log file: {e}")

1. Prerequisites

  • Python 3.8+ installed.
  • Darktable installed (specifically darktable-cli).
  • Images: A folder containing RAW files (e.g., .ARW, .CR3) and their corresponding .xmp sidecar files.

2. Installation

Open your terminal and install the required library:

Fish
pip install streamlit  

3. Configuration

Open the script (DT-Logs.py) in a text editor and adjust the Configuration Section at the top:

  • IMAGE_DIR: The full path to the folder containing your RAW files.
  • OUTPUT_DIR: The full path where the results (JPGs) and Logs should be saved.
  • DARKTABLE_CLI: Path to the executable (usually just "darktable-cli" if it is in your system PATH).

4. How to Run

Run the application from your terminal:

Fish
streamlit run DT-Logs.py

A browser window will open automatically (usually at http://localhost:8501).

5. Usage

  1. Select Images: Choose one or multiple images from the dropdown list.
  2. Settings:
    • Log Filename: Enter a name for the log file (a timestamped name is generated by default).
    • Debug Flags: Check the boxes for the Darktable debug modes you need (e.g., -d opencl, -d perf).
  3. Start: Click “Start Processing”.
  4. Results:
    • A progress bar shows the status.
    • Click “Show detailed logs” to see the live output from Darktable.
    • Once finished, the log file is saved to your output directory.