Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions FORK_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Fork notes (SandeepSubba/RapidRAW)

This fork tracks the upstream project [`CyberTimon/RapidRAW`](https://github.com/CyberTimon/RapidRAW)
and adds a small set of custom changes on top. This file documents what we
changed and how to re-apply it cleanly when upstream releases a new version.

## Git remotes

```
origin -> https://github.com/CyberTimon/RapidRAW.git (upstream / the original author)
fork -> https://github.com/SandeepSubba/RapidRAW.git (this fork)
```

> If `origin` points at the fork on your machine, rename it: `git remote rename
> origin fork` and add upstream as `git remote add origin https://github.com/CyberTimon/RapidRAW.git`.

## Branches

| Branch | Contents | Purpose |
| ------ | -------- | ------- |
| `custom-shortcuts` | backend hardening + refactors **+ the adjustment shortcuts** + this doc | The full fork — **run and rebase this one.** |
| `code-analysis-fixes` | backend hardening + refactors only | Clean subset to open as a PR to upstream. Does **not** include the personal shortcut feature. |

The two share the same 3 fix/refactor commits, so opening the PR from
`code-analysis-fixes` keeps the personal shortcuts out of the contribution.

## Custom changes in this fork

All changes are kept **small, isolated, and additive** specifically so they
survive upstream updates with minimal merge friction.

### 1. Capture One–style adjustment shortcuts (`feat:` commit)
Increase/decrease keyboard shortcuts for the core tonal & color sliders, shown
in **Settings → Controls → Adjustments** and fully remappable.

| File | Change | Conflict risk |
| ---- | ------ | ------------- |
| `src/utils/keyboardUtils.ts` | adds the `adjustments` section + the `ADJUSTMENT_NUDGES` table, spread into `KEYBIND_DEFINITIONS` | low (additive) |
| `src/hooks/useKeyboardShortcuts.ts` | one generated handler per nudge, after the `actions` map | low (additive) |
| `src/i18n/locales/en.json` | section label + action labels | low (additive) |

To change/extend: edit the single `ADJUSTMENT_NUDGES` array in
`keyboardUtils.ts` (combo, target adjustment key, step, clamp range). The
dispatcher and the keybind UI are both driven from it, so nothing else needs
to change.

### 2. Metadata export-naming tokens + single-image file naming (`feat:` commit)
Export filename templates gain metadata tokens that mirror the Metadata panel's
editable fields, the File Naming section shows for single-image export (not just
batch), and unknown tokens fall back to the default template.

| File | Change | Conflict risk |
| ---- | ------ | ------------- |
| `src-tauri/src/file_management.rs` | `{title}`/`{author}`/`{copyright}`/`{comments}` substitution + `sanitize_filename_component` + `generate_export_filename` command | low (additive) |
| `src-tauri/src/lib.rs` | registers `generate_export_filename` | low (additive) |
| `src/components/ui/ExportImportProperties.tsx` | new token list entries + `DEFAULT_FILENAME_TEMPLATE` + `sanitizeFilenameTemplate` | low (additive) |
| `src/components/panel/right/ExportPanel.tsx` | show naming UI for single image; resolve single-image name via backend | low |
| `src/hooks/useExportSettings.ts` | sanitize template on preset apply | low |
| `src/components/ui/AppProperties.tsx` | `GenerateExportFilename` invoke enum entry | low (additive) |

To add a metadata token: add the substitution in `generate_filename_from_template`
and the `{token}` string to `FILENAME_VARIABLES`.

### 3. Backend hardening + refactors (`fix:` / `refactor:` commits)
See the `code-analysis-fixes` branch / the open PR to upstream. These are
candidates to be merged upstream; if they are, drop them from the fork.

## Updating when upstream releases a new version

```bash
# 1. Get the latest upstream code
git fetch origin

# 2. Replay our custom commits on top of the new upstream main
git checkout custom-shortcuts
git rebase origin/main

# 3. If a conflict appears (rare, since our changes are additive), fix the
# file, then:
git add <file>
git rebase --continue

# 4. Reinstall deps in case package.json changed upstream, then test
npm install
npm start

# 5. Update the fork
git push --force-with-lease fork custom-shortcuts
```

Because the feature lives in one additive commit, the usual outcome of step 2
is a clean replay with no conflicts. If upstream ever restructures
`keyboardUtils.ts` or `useKeyboardShortcuts.ts`, the only fix-up needed is to
re-add the `ADJUSTMENT_NUDGES` block and its dispatch loop — both are clearly
commented in the source.

## Build / run

```bash
npm install # Node.js LTS + Rust toolchain required
npm run typecheck # note: upstream has pre-existing strict-tsc errors; the
# Vite/esbuild build does not gate on them
npm start # tauri dev — builds the Rust backend and launches the app
```
74 changes: 64 additions & 10 deletions src-tauri/src/ai_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,16 @@ pub fn run_lama_inpainting(
outputs[0].try_extract_array::<f32>()?.to_owned()
};

let out_dims = output_tensor.shape();
if out_dims.len() < 4 || (out_dims[2] as u32) < fh || (out_dims[3] as u32) < fw {
return Err(anyhow::anyhow!(
"LaMa output shape {:?} smaller than expected {}x{}",
out_dims,
fw,
fh
));
}

let mut result_inf = RgbaImage::new(fw, fh);
for y in 0..fh {
for x in 0..fw {
Expand Down Expand Up @@ -1045,11 +1055,27 @@ pub fn run_sam_decoder(
};

let mask_dims = mask_tensor.shape();
if mask_dims.len() < 4 {
return Err(anyhow::anyhow!(
"Unexpected SAM decoder output rank: expected 4 dims, got {:?}",
mask_dims
));
}
let h = mask_dims[2];
let w = mask_dims[3];
let area = h * w;

let mask_slice = mask_tensor.as_slice().unwrap();
let mask_slice = mask_tensor
.as_slice()
.ok_or_else(|| anyhow::anyhow!("SAM decoder output tensor is not contiguous"))?;
if mask_slice.len() < area {
return Err(anyhow::anyhow!(
"SAM decoder output too small: {} elements for {}x{} mask",
mask_slice.len(),
w,
h
));
}
let first_mask_slice = &mask_slice[0..area];

if i == iters - 1 {
Expand Down Expand Up @@ -1156,9 +1182,11 @@ pub fn run_sam_decoder(
.collect();

let img_mask_f32 =
ImageBuffer::<Luma<f32>, Vec<f32>>::from_raw(w as u32, h as u32, mask_f32_vec).unwrap();
ImageBuffer::<Luma<f32>, Vec<f32>>::from_raw(w as u32, h as u32, mask_f32_vec)
.ok_or_else(|| anyhow::anyhow!("Failed to build mask buffer from SAM output"))?;
let img_gaus_f32 =
ImageBuffer::<Luma<f32>, Vec<f32>>::from_raw(w as u32, h as u32, gaus_dt).unwrap();
ImageBuffer::<Luma<f32>, Vec<f32>>::from_raw(w as u32, h as u32, gaus_dt)
.ok_or_else(|| anyhow::anyhow!("Failed to build gaussian buffer from SAM output"))?;

let resized_mask = imageops::resize(&img_mask_f32, 256, 256, FilterType::Triangle);
let resized_gaus = imageops::resize(&img_gaus_f32, 256, 256, FilterType::Triangle);
Expand All @@ -1177,7 +1205,7 @@ pub fn run_sam_decoder(
}

mask_input = Array::from_shape_vec((1, 1, 256, 256), mask_input_flat)
.unwrap()
.map_err(|e| anyhow::anyhow!("Failed to reshape SAM mask input: {e}"))?
.into_dyn();
has_mask_input = 1.0;
}
Expand Down Expand Up @@ -1234,7 +1262,17 @@ pub fn run_sky_seg_model(
let mut session = sky_seg_session.lock().unwrap();
let outputs = session.run(ort::inputs![t_input])?;
let output_tensor = outputs[0].try_extract_array::<f32>()?.to_owned();
let out_slice = output_tensor.as_slice().unwrap();
let usize_size = SKYSEG_INPUT_SIZE as usize;
let out_slice = output_tensor
.as_slice()
.ok_or_else(|| anyhow::anyhow!("Sky Segmentation output tensor is not contiguous"))?;
if out_slice.len() < usize_size * usize_size {
return Err(anyhow::anyhow!(
"Sky Segmentation output too small: {} elements, need {}",
out_slice.len(),
usize_size * usize_size
));
}

let mut min_val = f32::MAX;
let mut max_val = f32::MIN;
Expand All @@ -1246,7 +1284,6 @@ pub fn run_sky_seg_model(
let range = max_val - min_val;
let scale = if range > 1e-6 { 255.0 / range } else { 0.0 };

let usize_size = SKYSEG_INPUT_SIZE as usize;
let mut cropped_mask_data = Vec::with_capacity(rw * rh);

for y in 0..rh {
Expand Down Expand Up @@ -1315,7 +1352,17 @@ pub fn run_u2netp_model(
let mut session = u2netp_session.lock().unwrap();
let outputs = session.run(ort::inputs![t_input])?;
let output_tensor = outputs[0].try_extract_array::<f32>()?.to_owned();
let out_slice = output_tensor.as_slice().unwrap();
let usize_size = U2NETP_INPUT_SIZE as usize;
let out_slice = output_tensor
.as_slice()
.ok_or_else(|| anyhow::anyhow!("U-2-Netp output tensor is not contiguous"))?;
if out_slice.len() < usize_size * usize_size {
return Err(anyhow::anyhow!(
"U-2-Netp output too small: {} elements, need {}",
out_slice.len(),
usize_size * usize_size
));
}

let mut min_val = f32::MAX;
let mut max_val = f32::MIN;
Expand All @@ -1327,7 +1374,6 @@ pub fn run_u2netp_model(
let range = max_val - min_val;
let scale = if range > 1e-6 { 255.0 / range } else { 0.0 };

let usize_size = U2NETP_INPUT_SIZE as usize;
let mut cropped_mask_data = Vec::with_capacity(rw * rh);

for y in 0..rh {
Expand Down Expand Up @@ -1394,9 +1440,17 @@ pub fn run_depth_anything_model(
let mut session = depth_session.lock().unwrap();
let outputs = session.run(ort::inputs![t_input])?;
let output_tensor = outputs[0].try_extract_array::<f32>()?.to_owned();
let out_slice = output_tensor.as_slice().unwrap();

let usize_size = DEPTH_INPUT_SIZE as usize;
let out_slice = output_tensor
.as_slice()
.ok_or_else(|| anyhow::anyhow!("Depth Anything output tensor is not contiguous"))?;
if out_slice.len() < usize_size * usize_size {
return Err(anyhow::anyhow!(
"Depth Anything output too small: {} elements, need {}",
out_slice.len(),
usize_size * usize_size
));
}

let mut min_val = f32::MAX;
let mut max_val = f32::MIN;
Expand Down
18 changes: 9 additions & 9 deletions src-tauri/src/cache_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,26 +112,26 @@ pub fn calculate_transform_hash(adjustments: &serde_json::Value) -> u64 {
is_visible.hash(&mut hasher);

if let Some(patch_data) = patch.get("patchData") {
let color_len = patch_data
// Hash the full base64 content, not just its length: two different
// patch results of identical dimensions encode to the same length
// and would otherwise collide, leaving the stale patch composited.
patch_data
.get("color")
.and_then(|v| v.as_str())
.unwrap_or("")
.len();
color_len.hash(&mut hasher);
.hash(&mut hasher);

let mask_len = patch_data
patch_data
.get("mask")
.and_then(|v| v.as_str())
.unwrap_or("")
.len();
mask_len.hash(&mut hasher);
.hash(&mut hasher);
} else {
let data_len = patch
patch
.get("patchDataBase64")
.and_then(|v| v.as_str())
.unwrap_or("")
.len();
data_len.hash(&mut hasher);
.hash(&mut hasher);
}

if let Some(sub_masks_val) = patch.get("subMasks") {
Expand Down
10 changes: 9 additions & 1 deletion src-tauri/src/export_processing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ fn calculate_resize_target(
current_h: u32,
resize_opts: &ResizeOptions,
) -> (u32, u32) {
// Guard against zero-dimension images: the aspect-ratio math below divides by
// current_w / current_h and would otherwise produce Inf/NaN.
if current_w == 0 || current_h == 0 {
return (current_w, current_h);
}

if resize_opts.dont_enlarge {
let exceeds = match resize_opts.mode {
ResizeMode::LongEdge => current_w.max(current_h) > resize_opts.value,
Expand Down Expand Up @@ -369,7 +375,9 @@ fn build_single_mask_adjustments(all: &AllAdjustments, mask_index: usize) -> All
tile_offset_y: all.tile_offset_y,
mask_atlas_cols: all.mask_atlas_cols,
};
single.mask_adjustments[0] = all.mask_adjustments[mask_index];
if mask_index < all.mask_adjustments.len() {
single.mask_adjustments[0] = all.mask_adjustments[mask_index];
}
for i in 1..single.mask_adjustments.len() {
single.mask_adjustments[i] = Default::default();
}
Expand Down
Loading