Skip to content

Commit 92dd610

Browse files
committed
add universal presets importer
1 parent 6c6b1bd commit 92dd610

File tree

9 files changed

+344
-7
lines changed

9 files changed

+344
-7
lines changed

src-tauri/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ rand = "0.9"
5353
tauri-plugin-shell = "2.3.1"
5454
tempfile = "3.22.0"
5555
image_hasher = "3.0.0"
56+
regex = "1.11.2"
5657

5758
[build-dependencies]
5859
tauri-build = { version = "2.4", features = [] }

src-tauri/src/file_management.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ use serde_json::Value;
2222
use tauri::{AppHandle, Emitter, Manager};
2323
use uuid::Uuid;
2424
use walkdir::WalkDir;
25+
use regex::Regex;
2526

2627
use crate::AppState;
2728
use crate::formats::is_supported_image_file;
@@ -34,6 +35,7 @@ use crate::image_processing::{
3435
};
3536
use crate::mask_generation::{MaskDefinition, generate_mask_bitmap};
3637
use crate::tagging::COLOR_TAG_PREFIX;
38+
use crate::preset_converter;
3739

3840
const THUMBNAIL_WIDTH: u32 = 640;
3941

@@ -1349,6 +1351,59 @@ pub fn handle_import_presets_from_file(
13491351
Ok(current_presets)
13501352
}
13511353

1354+
#[tauri::command]
1355+
pub fn handle_import_legacy_presets_from_file(
1356+
file_path: String,
1357+
app_handle: AppHandle,
1358+
) -> Result<Vec<PresetItem>, String> {
1359+
let content = fs::read_to_string(&file_path)
1360+
.map_err(|e| format!("Failed to read legacy preset file: {}", e))?;
1361+
1362+
let xmp_content = if file_path.to_lowercase().ends_with(".lrtemplate") {
1363+
let re = Regex::new(r#"(?s)s.xmp = "(.*)""#).unwrap();
1364+
if let Some(caps) = re.captures(&content) {
1365+
caps.get(1)
1366+
.map(|m| m.as_str().replace(r#"\""#, r#"""#))
1367+
.unwrap_or(content)
1368+
} else {
1369+
content
1370+
}
1371+
} else {
1372+
content
1373+
};
1374+
1375+
let converted_preset = preset_converter::convert_xmp_to_preset(&xmp_content)?;
1376+
1377+
let mut current_presets = load_presets(app_handle.clone())?;
1378+
1379+
let current_names: HashSet<String> = current_presets
1380+
.iter()
1381+
.flat_map(|item| match item {
1382+
PresetItem::Preset(p) => vec![p.name.clone()],
1383+
PresetItem::Folder(f) => {
1384+
let mut names = vec![f.name.clone()];
1385+
names.extend(f.children.iter().map(|c| c.name.clone()));
1386+
names
1387+
}
1388+
})
1389+
.collect();
1390+
1391+
let mut new_name = converted_preset.name.clone();
1392+
let mut counter = 1;
1393+
while current_names.contains(&new_name) {
1394+
new_name = format!("{} ({})", converted_preset.name, counter);
1395+
counter += 1;
1396+
}
1397+
1398+
let mut final_preset = converted_preset;
1399+
final_preset.name = new_name;
1400+
1401+
current_presets.push(PresetItem::Preset(final_preset));
1402+
1403+
save_presets(current_presets.clone(), app_handle)?;
1404+
Ok(current_presets)
1405+
}
1406+
13521407
#[tauri::command]
13531408
pub fn handle_export_presets_to_file(
13541409
presets_to_export: Vec<PresetItem>,

src-tauri/src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod panorama_utils;
1616
mod raw_processing;
1717
mod tagging;
1818
mod tagging_utils;
19+
mod preset_converter;
1920

2021
use std::thread;
2122
use std::collections::{HashMap, hash_map::DefaultHasher};
@@ -2131,6 +2132,7 @@ fn main() {
21312132
file_management::reset_adjustments_for_paths,
21322133
file_management::apply_auto_adjustments_to_paths,
21332134
file_management::handle_import_presets_from_file,
2135+
file_management::handle_import_legacy_presets_from_file,
21342136
file_management::handle_export_presets_to_file,
21352137
file_management::save_community_preset,
21362138
file_management::clear_all_sidecars,

src-tauri/src/preset_converter.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
use regex::Regex;
2+
use serde_json::{json, Map, Value};
3+
use std::collections::HashMap;
4+
use uuid::Uuid;
5+
6+
use crate::file_management::Preset;
7+
8+
#[derive(Copy, Clone, Debug)]
9+
enum Num {
10+
I(i64),
11+
F(f64),
12+
}
13+
14+
fn parse_num(s: &str) -> Option<Num> {
15+
if let Ok(i) = s.parse::<i64>() {
16+
Some(Num::I(i))
17+
} else if let Ok(f) = s.parse::<f64>() {
18+
Some(Num::F(f))
19+
} else {
20+
None
21+
}
22+
}
23+
24+
fn num_to_json(num: Num) -> Option<Value> {
25+
match num {
26+
Num::I(i) => Some(Value::Number(i.into())),
27+
Num::F(f) => serde_json::Number::from_f64(f).map(Value::Number),
28+
}
29+
}
30+
31+
fn get_attr_as_f64(attrs: &HashMap<String, String>, key: &str) -> Option<f64> {
32+
attrs.get(key)
33+
.and_then(|s| s.trim_start_matches('+').parse::<f64>().ok())
34+
}
35+
36+
fn extract_xmp_name(xmp_content: &str) -> Option<String> {
37+
let re = Regex::new(r#"(?s)<crs:Name>.*?<rdf:Alt>.*?<rdf:li[^>]*>([^<]+)</rdf:li>.*?</crs:Name>"#)
38+
.ok()?;
39+
re.captures(xmp_content)
40+
.and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()))
41+
}
42+
43+
fn extract_tone_curve_points(xmp_str: &str, curve_name: &str) -> Option<Vec<Value>> {
44+
let pattern = format!(
45+
r"(?s)<crs:{}>\s*<rdf:Seq>(.*?)</rdf:Seq>\s*</crs:{}>",
46+
curve_name, curve_name
47+
);
48+
let re = Regex::new(&pattern).ok()?;
49+
let captures = re.captures(xmp_str)?;
50+
let seq_content = captures.get(1)?.as_str();
51+
52+
let point_re = Regex::new(r"<rdf:li>(\d+),\s*(\d+)</rdf:li>").ok()?;
53+
let mut points = Vec::new();
54+
55+
for point_cap in point_re.captures_iter(seq_content) {
56+
let x: u32 = point_cap.get(1)?.as_str().parse().ok()?;
57+
let y: u32 = point_cap.get(2)?.as_str().parse().ok()?;
58+
59+
let mut final_y = y;
60+
if curve_name == "ToneCurvePV2012" {
61+
const SHADOW_RANGE_END: f64 = 64.0;
62+
const SHADOW_DAMPEN_START: f64 = 0.6;
63+
const SHADOW_DAMPEN_END: f64 = 1.0;
64+
65+
let x_f64 = x as f64;
66+
let y_f64 = y as f64;
67+
68+
if y_f64 > x_f64 && x_f64 < SHADOW_RANGE_END {
69+
let lift_amount = y_f64 - x_f64;
70+
let progress = x_f64 / SHADOW_RANGE_END;
71+
let dampening_factor = SHADOW_DAMPEN_START + (SHADOW_DAMPEN_END - SHADOW_DAMPEN_START) * progress;
72+
73+
let new_y = x_f64 + (lift_amount * dampening_factor);
74+
final_y = new_y.round().clamp(0.0, 255.0) as u32;
75+
}
76+
}
77+
78+
let mut point = Map::new();
79+
point.insert("x".to_string(), Value::Number(x.into()));
80+
point.insert("y".to_string(), Value::Number(final_y.into()));
81+
points.push(Value::Object(point));
82+
}
83+
84+
if points.is_empty() {
85+
None
86+
} else {
87+
Some(points)
88+
}
89+
}
90+
91+
pub fn convert_xmp_to_preset(xmp_content: &str) -> Result<Preset, String> {
92+
let xmp_one_line = xmp_content.split('\n').collect::<Vec<_>>().join(" ");
93+
94+
let attr_re = Regex::new(r#"crs:([A-Za-z0-9]+)="([^"]*)""#)
95+
.map_err(|e| format!("Regex compilation failed: {}", e))?;
96+
let mut attrs: HashMap<String, String> = HashMap::new();
97+
for cap in attr_re.captures_iter(&xmp_one_line) {
98+
attrs.insert(cap[1].to_string(), cap[2].to_string());
99+
}
100+
101+
let mut adjustments = Map::new();
102+
let mut hsl_map = Map::new();
103+
let mut color_grading_map = Map::new();
104+
let mut curves_map = Map::new();
105+
106+
let mappings = vec![
107+
("Exposure2012", "exposure"),
108+
("Contrast2012", "contrast"),
109+
("Highlights2012", "highlights"),
110+
("Whites2012", "whites"),
111+
("Blacks2012", "blacks"),
112+
("Clarity2012", "clarity"),
113+
("Dehaze", "dehaze"),
114+
("Vibrance", "vibrance"),
115+
("Saturation", "saturation"),
116+
("Texture", "structure"),
117+
("SharpenRadius", "sharpenRadius"),
118+
("SharpenDetail", "sharpenDetail"),
119+
("SharpenEdgeMasking", "sharpenMasking"),
120+
("LuminanceSmoothing", "lumaNoiseReduction"),
121+
("ColorNoiseReduction", "colorNoiseReduction"),
122+
("ColorNoiseReductionDetail", "colorNoiseDetail"),
123+
("ColorNoiseReductionSmoothness", "colorNoiseSmoothness"),
124+
("ChromaticAberrationRedCyan", "chromaticAberrationRedCyan"),
125+
("ChromaticAberrationBlueYellow", "chromaticAberrationBlueYellow"),
126+
("PostCropVignetteAmount", "vignetteAmount"),
127+
("PostCropVignetteMidpoint", "vignetteMidpoint"),
128+
("PostCropVignetteFeather", "vignetteFeather"),
129+
("PostCropVignetteRoundness", "vignetteRoundness"),
130+
("GrainAmount", "grainAmount"),
131+
("GrainSize", "grainSize"),
132+
("GrainFrequency", "grainRoughness"),
133+
("ColorGradeBlending", "blending"),
134+
];
135+
136+
for (xmp_key, rr_key) in mappings {
137+
if let Some(raw_val) = attrs.get(xmp_key) {
138+
if let Some(num) = parse_num(raw_val.trim_start_matches('+')) {
139+
if let Some(json_val) = num_to_json(num) {
140+
if rr_key == "blending" {
141+
color_grading_map.insert(rr_key.to_string(), json_val);
142+
} else {
143+
adjustments.insert(rr_key.to_string(), json_val);
144+
}
145+
}
146+
}
147+
}
148+
}
149+
150+
if let Some(shadows_val) = get_attr_as_f64(&attrs, "Shadows2012") {
151+
let adjusted_shadows = (shadows_val * 1.5).min(100.0);
152+
adjustments.insert("shadows".to_string(), json!(adjusted_shadows));
153+
}
154+
adjustments.insert("sharpness".to_string(), json!(0));
155+
156+
if let Some(adjusted_k) = get_attr_as_f64(&attrs, "Temperature") {
157+
const AS_SHOT_DEFAULT: f64 = 5500.0;
158+
const MAX_MIRED_SHIFT: f64 = 150.0;
159+
let as_shot_k = get_attr_as_f64(&attrs, "AsShotTemperature").unwrap_or(AS_SHOT_DEFAULT);
160+
let mired_adjusted = 1_000_000.0 / adjusted_k;
161+
let mired_as_shot = 1_000_000.0 / as_shot_k;
162+
let mired_delta = mired_adjusted - mired_as_shot;
163+
let temp_value = (-mired_delta / MAX_MIRED_SHIFT) * 100.0;
164+
adjustments.insert("temperature".to_string(), json!(temp_value.clamp(-100.0, 100.0)));
165+
}
166+
167+
if let Some(tint_val) = get_attr_as_f64(&attrs, "Tint") {
168+
let scaled_tint = (tint_val / 150.0) * 100.0;
169+
adjustments.insert("tint".to_string(), json!(scaled_tint.clamp(-100.0, 100.0)));
170+
}
171+
172+
let colors = [
173+
("Red", "reds"), ("Orange", "oranges"), ("Yellow", "yellows"),
174+
("Green", "greens"), ("Aqua", "aquas"), ("Blue", "blues"),
175+
("Purple", "purples"), ("Magenta", "magentas"),
176+
];
177+
for (src, dst) in colors {
178+
let mut color_map = Map::new();
179+
if let Some(raw) = attrs.get(&format!("HueAdjustment{}", src)) {
180+
if let Some(num) = parse_num(raw.trim_start_matches('+')) {
181+
if let Some(Value::Number(n)) = num_to_json(num) {
182+
if let Some(val_f64) = n.as_f64() {
183+
let halved_hue = val_f64 * 0.5;
184+
color_map.insert("hue".to_string(), json!(halved_hue));
185+
}
186+
}
187+
}
188+
}
189+
if let Some(raw) = attrs.get(&format!("SaturationAdjustment{}", src)) {
190+
if let Some(num) = parse_num(raw.trim_start_matches('+')) {
191+
if let Some(json_val) = num_to_json(num) { color_map.insert("saturation".to_string(), json_val); }
192+
}
193+
}
194+
if let Some(raw) = attrs.get(&format!("LuminanceAdjustment{}", src)) {
195+
if let Some(num) = parse_num(raw.trim_start_matches('+')) {
196+
if let Some(json_val) = num_to_json(num) { color_map.insert("luminance".to_string(), json_val); }
197+
}
198+
}
199+
if !color_map.is_empty() {
200+
hsl_map.insert(dst.to_string(), Value::Object(color_map));
201+
}
202+
}
203+
if !hsl_map.is_empty() {
204+
adjustments.insert("hsl".to_string(), Value::Object(hsl_map));
205+
}
206+
207+
let mut shadows_map = Map::new();
208+
let mut midtones_map = Map::new();
209+
let mut highlights_map = Map::new();
210+
if let Some(raw) = attrs.get("SplitToningShadowHue") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { shadows_map.insert("hue".to_string(), json_val); }}}
211+
if let Some(raw) = attrs.get("ColorGradeMidtoneHue") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { midtones_map.insert("hue".to_string(), json_val); }}}
212+
if let Some(raw) = attrs.get("SplitToningHighlightHue") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { highlights_map.insert("hue".to_string(), json_val); }}}
213+
if let Some(raw) = attrs.get("SplitToningShadowSaturation") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { shadows_map.insert("saturation".to_string(), json_val); }}}
214+
if let Some(raw) = attrs.get("ColorGradeMidtoneSat") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { midtones_map.insert("saturation".to_string(), json_val); }}}
215+
if let Some(raw) = attrs.get("SplitToningHighlightSaturation") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { highlights_map.insert("saturation".to_string(), json_val); }}}
216+
if let Some(raw) = attrs.get("ColorGradeShadowLum") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { shadows_map.insert("luminance".to_string(), json_val); }}}
217+
if let Some(raw) = attrs.get("ColorGradeMidtoneLum") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { midtones_map.insert("luminance".to_string(), json_val); }}}
218+
if let Some(raw) = attrs.get("ColorGradeHighlightLum") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { highlights_map.insert("luminance".to_string(), json_val); }}}
219+
if let Some(raw) = attrs.get("SplitToningBalance") { if let Some(num) = parse_num(raw) { if let Some(json_val) = num_to_json(num) { color_grading_map.insert("balance".to_string(), json_val); }}}
220+
if !shadows_map.is_empty() { color_grading_map.insert("shadows".to_string(), Value::Object(shadows_map)); }
221+
if !midtones_map.is_empty() { color_grading_map.insert("midtones".to_string(), Value::Object(midtones_map)); }
222+
if !highlights_map.is_empty() { color_grading_map.insert("highlights".to_string(), Value::Object(highlights_map)); }
223+
if !color_grading_map.is_empty() { adjustments.insert("colorGrading".to_string(), Value::Object(color_grading_map)); }
224+
225+
let curve_mappings = [
226+
("ToneCurvePV2012", "luma"), ("ToneCurvePV2012Red", "red"),
227+
("ToneCurvePV2012Green", "green"), ("ToneCurvePV2012Blue", "blue"),
228+
];
229+
for (xmp_curve, rr_curve) in curve_mappings {
230+
if let Some(points) = extract_tone_curve_points(xmp_content, xmp_curve) {
231+
curves_map.insert(rr_curve.to_string(), Value::Array(points));
232+
}
233+
}
234+
if !curves_map.is_empty() {
235+
adjustments.insert("curves".to_string(), Value::Object(curves_map));
236+
}
237+
238+
let preset_name = extract_xmp_name(xmp_content).unwrap_or_else(|| "Imported Preset".to_string());
239+
240+
Ok(Preset {
241+
id: Uuid::new_v4().to_string(),
242+
name: preset_name,
243+
adjustments: Value::Object(adjustments),
244+
})
245+
}

src/components/panel/right/PresetsPanel.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ export default function PresetsPanel({
265265
duplicatePreset,
266266
exportPresetsToFile,
267267
importPresetsFromFile,
268+
importLegacyPresetsFromFile,
268269
isLoading,
269270
movePreset,
270271
presets,
@@ -643,13 +644,24 @@ export default function PresetsPanel({
643644
const handleImportPresets = async () => {
644645
try {
645646
const selectedPath = await openDialog({
646-
filters: [{ name: 'Preset File', extensions: ['rrpreset'] }],
647+
filters: [
648+
{ name: 'All Preset Files', extensions: ['rrpreset', 'xmp', 'lrtemplate'] },
649+
{ name: 'RapidRAW Preset', extensions: ['rrpreset'] },
650+
{ name: 'Legacy Preset', extensions: ['xmp', 'lrtemplate'] },
651+
],
647652
multiple: false,
648653
title: 'Import Presets',
649654
});
650655

651656
if (typeof selectedPath === 'string') {
652-
await importPresetsFromFile(selectedPath);
657+
const isLegacy = selectedPath.toLowerCase().endsWith('.xmp') || selectedPath.toLowerCase().endsWith('.lrtemplate');
658+
659+
if (isLegacy) {
660+
await importLegacyPresetsFromFile(selectedPath);
661+
} else {
662+
await importPresetsFromFile(selectedPath);
663+
}
664+
653665
setFolderPreviewsGenerated(new Set<string>());
654666
setPreviews({});
655667
}

src/components/ui/AppProperties.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export enum Invokes {
3838
GetSupportedFileTypes = 'get_supported_file_types',
3939
HandleExportPresetsToFile = 'handle_export_presets_to_file',
4040
HandleImportPresetsFromFile = 'handle_import_presets_from_file',
41+
HandleImportLegacyPresetsFromFile = 'handle_import_legacy_presets_from_file',
4142
ImportFiles = 'import_files',
4243
InvokeGenerativeReplace = 'invoke_generative_replace',
4344
InvokeGenerativeReplaseWithMaskDef = 'invoke_generative_replace_with_mask_def',

0 commit comments

Comments
 (0)