Skip to content

Commit 35f6e83

Browse files
committed
add masks & crop support for presets
1 parent b01aa02 commit 35f6e83

9 files changed

Lines changed: 216 additions & 60 deletions

File tree

src-tauri/src/file_management.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,13 @@ pub struct Preset {
7272
pub id: String,
7373
pub name: String,
7474
pub adjustments: Value,
75+
#[serde(rename = "includeMasks", skip_serializing_if = "Option::is_none")]
76+
pub include_masks: Option<bool>,
77+
#[serde(
78+
rename = "includeCropTransform",
79+
skip_serializing_if = "Option::is_none"
80+
)]
81+
pub include_crop_transform: Option<bool>,
7582
}
7683

7784
#[derive(Serialize)]
@@ -171,6 +178,7 @@ fn all_available_adjustments() -> HashSet<String> {
171178
"exposure",
172179
"brightness",
173180
"contrast",
181+
"curves",
174182
"highlights",
175183
"shadows",
176184
"whites",
@@ -3084,6 +3092,8 @@ pub fn save_community_preset(
30843092
name: String,
30853093
adjustments: Value,
30863094
app_handle: AppHandle,
3095+
include_masks: Option<bool>,
3096+
include_crop_transform: Option<bool>,
30873097
) -> Result<(), String> {
30883098
let mut current_presets = load_presets(app_handle.clone())?;
30893099

@@ -3112,6 +3122,8 @@ pub fn save_community_preset(
31123122
id: Uuid::new_v4().to_string(),
31133123
name,
31143124
adjustments,
3125+
include_masks,
3126+
include_crop_transform,
31153127
};
31163128

31173129
if let Some(PresetItem::Folder(folder)) = current_presets.iter_mut().find(|item| {

src-tauri/src/lib.rs

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,10 @@ pub struct CommunityPreset {
251251
pub name: String,
252252
pub creator: String,
253253
pub adjustments: Value,
254+
#[serde(rename = "includeMasks")]
255+
pub include_masks: Option<bool>,
256+
#[serde(rename = "includeCropTransform")]
257+
pub include_crop_transform: Option<bool>,
254258
}
255259

256260
#[derive(Serialize)]
@@ -3335,34 +3339,37 @@ fn generate_preset_preview(
33353339
.unwrap()
33363340
.clone()
33373341
.ok_or("No original image loaded for preset preview")?;
3338-
let original_image = loaded_image.image;
3339-
let path = loaded_image.path;
33403342
let is_raw = loaded_image.is_raw;
3341-
let unique_hash = calculate_full_job_hash(&path, &js_adjustments);
3343+
let unique_hash = calculate_full_job_hash(&loaded_image.path, &js_adjustments);
33423344

3343-
const PRESET_PREVIEW_DIM: u32 = 200;
3344-
let preview_base = downscale_f32_image(&original_image, PRESET_PREVIEW_DIM, PRESET_PREVIEW_DIM);
3345+
const PRESET_PREVIEW_DIM: u32 = 400;
33453346

3346-
let (transformed_image, unscaled_crop_offset) =
3347-
apply_all_transformations(Cow::Borrowed(&preview_base), &js_adjustments);
3348-
let (img_w, img_h) = transformed_image.dimensions();
3347+
let (preview_image, scale_for_gpu, unscaled_crop_offset) =
3348+
generate_transformed_preview(&state, &loaded_image, &js_adjustments, PRESET_PREVIEW_DIM)?;
3349+
3350+
let (img_w, img_h) = preview_image.dimensions();
33493351

33503352
let mask_definitions: Vec<MaskDefinition> = js_adjustments
33513353
.get("masks")
33523354
.and_then(|m| serde_json::from_value(m.clone()).ok())
33533355
.unwrap_or_default();
33543356

3355-
let warped_image = resolve_warped_image_for_masks(&state, &js_adjustments, &mask_definitions);
3357+
let scaled_crop_offset = (
3358+
unscaled_crop_offset.0 * scale_for_gpu,
3359+
unscaled_crop_offset.1 * scale_for_gpu,
3360+
);
3361+
33563362
let mask_bitmaps: Vec<ImageBuffer<Luma<u8>, Vec<u8>>> = mask_definitions
33573363
.iter()
33583364
.filter_map(|def| {
3359-
generate_mask_bitmap(
3365+
get_cached_or_generate_mask(
3366+
&state,
33603367
def,
33613368
img_w,
33623369
img_h,
3363-
1.0,
3364-
unscaled_crop_offset,
3365-
warped_image.as_deref(),
3370+
scale_for_gpu,
3371+
scaled_crop_offset,
3372+
&js_adjustments,
33663373
)
33673374
})
33683375
.collect();
@@ -3374,7 +3381,7 @@ fn generate_preset_preview(
33743381
let processed_image = process_and_get_dynamic_image(
33753382
&context,
33763383
&state,
3377-
transformed_image.as_ref(),
3384+
&preview_image,
33783385
unique_hash,
33793386
RenderRequest {
33803387
adjustments: all_adjustments,
@@ -3388,7 +3395,7 @@ fn generate_preset_preview(
33883395
let mut buf = Cursor::new(Vec::new());
33893396
processed_image
33903397
.to_rgb8()
3391-
.write_with_encoder(JpegEncoder::new_with_quality(&mut buf, 50))
3398+
.write_with_encoder(JpegEncoder::new_with_quality(&mut buf, 80))
33923399
.map_err(|e| e.to_string())?;
33933400

33943401
Ok(Response::new(buf.into_inner()))
@@ -3666,7 +3673,7 @@ async fn generate_all_community_previews(
36663673
let highlight_compression = settings.raw_highlight_compression.unwrap_or(2.5);
36673674
let linear_mode = settings.linear_raw_mode;
36683675

3669-
let mut base_thumbnails: Vec<(DynamicImage, bool)> = Vec::new();
3676+
let mut base_thumbnails: Vec<(DynamicImage, bool, f32)> = Vec::new();
36703677
for image_path in image_paths.iter() {
36713678
let (source_path, _) = parse_virtual_path(image_path);
36723679
let source_path_str = source_path.to_string_lossy().to_string();
@@ -3680,11 +3687,18 @@ async fn generate_all_community_previews(
36803687
None,
36813688
)
36823689
.map_err(|e| e.to_string())?;
3690+
36833691
let is_raw = is_raw_file(&source_path_str);
3684-
base_thumbnails.push((
3685-
downscale_f32_image(&original_image, PROCESSING_DIM, PROCESSING_DIM),
3686-
is_raw,
3687-
));
3692+
let (orig_w, orig_h) = original_image.dimensions();
3693+
let (base_image, base_scale) = if orig_w > PROCESSING_DIM || orig_h > PROCESSING_DIM {
3694+
let downscaled = downscale_f32_image(&original_image, PROCESSING_DIM, PROCESSING_DIM);
3695+
let scale = downscaled.width() as f32 / orig_w as f32;
3696+
(downscaled, scale)
3697+
} else {
3698+
(original_image, 1.0)
3699+
};
3700+
3701+
base_thumbnails.push((base_image, is_raw, base_scale));
36883702
}
36893703

36903704
for preset in presets.iter() {
@@ -3695,33 +3709,53 @@ async fn generate_all_community_previews(
36953709
preset.name.hash(&mut preset_hasher);
36963710
let preset_hash = preset_hasher.finish();
36973711

3698-
for (i, (base_image, is_raw)) in base_thumbnails.iter().enumerate() {
3699-
let (transformed_image, unscaled_crop_offset) =
3700-
crate::apply_all_transformations(Cow::Borrowed(base_image), js_adjustments);
3712+
for (i, (base_image, is_raw, base_scale)) in base_thumbnails.iter().enumerate() {
3713+
let mut scaled_adjustments = js_adjustments.clone();
3714+
if let Some(crop_val) = scaled_adjustments.get_mut("crop")
3715+
&& let Ok(c) = serde_json::from_value::<Crop>(crop_val.clone())
3716+
{
3717+
*crop_val = serde_json::to_value(Crop {
3718+
x: c.x * (*base_scale as f64),
3719+
y: c.y * (*base_scale as f64),
3720+
width: c.width * (*base_scale as f64),
3721+
height: c.height * (*base_scale as f64),
3722+
})
3723+
.unwrap_or(serde_json::Value::Null);
3724+
}
3725+
3726+
let (transformed_image, _scaled_crop_offset) =
3727+
crate::apply_all_transformations(Cow::Borrowed(base_image), &scaled_adjustments);
37013728
let (img_w, img_h) = transformed_image.dimensions();
37023729

3703-
let mask_definitions: Vec<MaskDefinition> = js_adjustments
3730+
let mask_definitions: Vec<MaskDefinition> = scaled_adjustments
37043731
.get("masks")
37053732
.and_then(|m| serde_json::from_value(m.clone()).ok())
37063733
.unwrap_or_else(Vec::new);
37073734

3708-
let warped_image =
3709-
resolve_warped_image_for_masks(&state, js_adjustments, &mask_definitions);
3735+
let unscaled_crop_offset = js_adjustments
3736+
.get("crop")
3737+
.and_then(|c| serde_json::from_value::<Crop>(c.clone()).ok())
3738+
.map_or((0.0, 0.0), |c| (c.x as f32, c.y as f32));
3739+
let actual_scaled_crop_offset = (
3740+
unscaled_crop_offset.0 * base_scale,
3741+
unscaled_crop_offset.1 * base_scale,
3742+
);
3743+
37103744
let mask_bitmaps: Vec<ImageBuffer<Luma<u8>, Vec<u8>>> = mask_definitions
37113745
.iter()
37123746
.filter_map(|def| {
37133747
generate_mask_bitmap(
37143748
def,
37153749
img_w,
37163750
img_h,
3717-
1.0,
3718-
unscaled_crop_offset,
3719-
warped_image.as_deref(),
3751+
*base_scale,
3752+
actual_scaled_crop_offset,
3753+
None,
37203754
)
37213755
})
37223756
.collect();
37233757

3724-
let all_adjustments = get_all_adjustments_from_json(js_adjustments, *is_raw);
3758+
let all_adjustments = get_all_adjustments_from_json(&scaled_adjustments, *is_raw);
37253759
let lut_path = js_adjustments["lutPath"].as_str();
37263760
let lut = lut_path.and_then(|p| get_or_load_lut(&state, p).ok());
37273761

src-tauri/src/preset_converter.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,5 +344,7 @@ pub fn convert_xmp_to_preset(xmp_content: &str) -> Result<Preset, String> {
344344
id: Uuid::new_v4().to_string(),
345345
name: preset_name,
346346
adjustments: Value::Object(adjustments),
347+
include_masks: None,
348+
include_crop_transform: None,
347349
})
348350
}

src/components/modals/AddPresetModal.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import Text from '../ui/Text';
33
import { TextVariants } from '../../types/typography';
4+
import Switch from '../ui/Switch';
45

56
interface PresetModalProps {
67
isOpen: boolean;
78
onClose(): void;
8-
onSave(name: string): void;
9+
onSave(name: string, includeMasks: boolean, includeCropTransform: boolean): void;
910
}
1011

1112
export default function AddPresetModal({ isOpen, onClose, onSave }: PresetModalProps) {
1213
const [name, setName] = useState('');
14+
const [includeMasks, setIncludeMasks] = useState(false);
15+
const [includeCropTransform, setIncludeCropTransform] = useState(false);
1316
const [isMounted, setIsMounted] = useState(false);
1417
const [show, setShow] = useState(false);
1518

@@ -23,17 +26,19 @@ export default function AddPresetModal({ isOpen, onClose, onSave }: PresetModalP
2326
const timer = setTimeout(() => {
2427
setIsMounted(false);
2528
setName('');
29+
setIncludeMasks(false);
30+
setIncludeCropTransform(false);
2631
}, 300);
2732
return () => clearTimeout(timer);
2833
}
2934
}, [isOpen]);
3035

3136
const handleSave = useCallback(() => {
3237
if (name.trim()) {
33-
onSave(name.trim());
38+
onSave(name.trim(), includeMasks, includeCropTransform);
3439
onClose();
3540
}
36-
}, [name, onSave, onClose]);
41+
}, [name, includeMasks, includeCropTransform, onSave, onClose]);
3742

3843
const handleKeyDown = useCallback(
3944
(e: any) => {
@@ -53,8 +58,8 @@ export default function AddPresetModal({ isOpen, onClose, onSave }: PresetModalP
5358
return (
5459
<div
5560
className={`
56-
fixed inset-0 flex items-center justify-center z-50
57-
bg-black/30 backdrop-blur-xs
61+
fixed inset-0 flex items-center justify-center z-50
62+
bg-black/30 backdrop-blur-xs
5863
transition-opacity duration-300 ease-in-out
5964
${show ? 'opacity-100' : 'opacity-0'}
6065
`}
@@ -64,7 +69,7 @@ export default function AddPresetModal({ isOpen, onClose, onSave }: PresetModalP
6469
>
6570
<div
6671
className={`
67-
bg-surface rounded-lg shadow-xl p-6 w-full max-w-sm
72+
bg-surface rounded-lg shadow-xl p-6 w-full max-w-sm
6873
transform transition-all duration-300 ease-out
6974
${show ? 'scale-100 opacity-100 translate-y-0' : 'scale-95 opacity-0 -translate-y-4'}
7075
`}
@@ -75,14 +80,20 @@ export default function AddPresetModal({ isOpen, onClose, onSave }: PresetModalP
7580
</Text>
7681
<input
7782
autoFocus
78-
className="w-full bg-bg-primary text-text-primary border border-border rounded-md px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-accent"
83+
className="w-full bg-bg-primary text-text-primary border border-border rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-accent"
7984
onChange={(e: any) => setName(e.target.value)}
8085
onKeyDown={handleKeyDown}
8186
placeholder="Enter preset name..."
8287
type="text"
8388
value={name}
8489
/>
85-
<div className="flex justify-end gap-3 mt-5">
90+
91+
<div className="mt-5 space-y-4 p-1">
92+
<Switch label="Include Masks" checked={includeMasks} onChange={setIncludeMasks} />
93+
<Switch label="Include Crop & Transform" checked={includeCropTransform} onChange={setIncludeCropTransform} />
94+
</div>
95+
96+
<div className="flex justify-end gap-3 mt-6">
8697
<button
8798
className="px-4 py-2 rounded-md text-text-secondary hover:bg-surface transition-colors"
8899
onClick={onClose}

src/components/panel/CommunityPage.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
22
import { invoke } from '@tauri-apps/api/core';
3-
import { ArrowLeft, CheckCircle2, ChevronDown, Loader2, Search, Users, Github } from 'lucide-react';
3+
import { ArrowLeft, CheckCircle2, ChevronDown, Loader2, Search, Users, Layers, Scaling, Github } from 'lucide-react';
44
import { motion, AnimatePresence } from 'framer-motion';
55
import Button from '../ui/Button';
66
import Input from '../ui/Input';
@@ -16,6 +16,8 @@ interface CommunityPreset {
1616
name: string;
1717
creator: string;
1818
adjustments: Record<string, any>;
19+
includeMasks?: boolean;
20+
includeCropTransform?: boolean;
1921
}
2022

2123
const SORT_METHODS: {
@@ -181,6 +183,8 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun
181183
await invoke(Invokes.SaveCommunityPreset, {
182184
name: preset.name,
183185
adjustments: preset.adjustments,
186+
includeMasks: preset.includeMasks,
187+
includeCropTransform: preset.includeCropTransform,
184188
});
185189
setDownloadStatus((prev) => ({ ...prev, [preset.name]: 'success' }));
186190
} catch (error) {
@@ -305,13 +309,17 @@ const CommunityPage = ({ onBackToLibrary, imageList, currentFolderPath }: Commun
305309
</Button>
306310
</div>
307311
</div>
308-
<div className="p-4 text-center">
312+
<div className="p-4 text-center relative">
313+
{/* Existing Name and Creator text */}
309314
<Text variant={TextVariants.heading} className="truncate mb-1">
310315
{preset.name}
311316
</Text>
312-
<Text variant={TextVariants.small} className="font-['cursive'] italic">
313-
by {preset.creator}
314-
</Text>
317+
318+
{/* Optional: Add small icons next to the name if the preset supports them */}
319+
<div className="flex justify-center gap-2 mt-1 opacity-60">
320+
{preset.includeMasks && <Layers size={12} title="Includes Masks" />}
321+
{preset.includeCropTransform && <Scaling size={12} title="Includes Geometry" />}
322+
</div>
315323
</div>
316324
</motion.div>
317325
);

0 commit comments

Comments
 (0)