Skip to content

Commit 9b014ca

Browse files
authored
Merge pull request CyberTimon#1383 from alexdhill/bugfix/miscellaneous
Minor bug patches
2 parents 026a78f + 1ee5314 commit 9b014ca

24 files changed

Lines changed: 278 additions & 51 deletions

src-tauri/src/export_processing.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,23 @@ fn export_adjustments_as_lut(
697697
convert_image_to_cube_lut(&processed_lut, lut_size)
698698
}
699699

700+
struct ExportHandleGuide {
701+
app_handle: tauri::AppHandle,
702+
}
703+
704+
impl Drop for ExportHandleGuide {
705+
fn drop(&mut self) {
706+
if let Ok(mut handle_lock) = self
707+
.app_handle
708+
.state::<AppState>()
709+
.export_task_handle
710+
.lock()
711+
{
712+
*handle_lock = None;
713+
}
714+
}
715+
}
716+
700717
#[allow(clippy::too_many_arguments)]
701718
#[tauri::command]
702719
pub async fn export_images(
@@ -730,12 +747,12 @@ pub async fn export_images(
730747

731748
let available_ram_gb = sys.available_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
732749

733-
let ram_based_limit = (available_ram_gb / 2.5).floor() as usize;
750+
let ram_based_limit = (available_ram_gb / 4.0).floor() as usize;
734751

735752
let num_threads = if paths.len() == 1 {
736753
1
737754
} else {
738-
available_cores.min(ram_based_limit).clamp(1, 16)
755+
available_cores.min(ram_based_limit).clamp(1, 4)
739756
};
740757

741758
log::info!(
@@ -746,6 +763,9 @@ pub async fn export_images(
746763
);
747764

748765
let task = tokio::spawn(async move {
766+
let _export_guard = ExportHandleGuide {
767+
app_handle: app_handle.clone(),
768+
};
749769
let output_folder_path = std::path::Path::new(&output_folder_or_file);
750770
let total_paths = paths.len();
751771
let settings = load_settings(app_handle.clone()).unwrap_or_default();
@@ -1036,12 +1056,6 @@ pub async fn export_images(
10361056
);
10371057
let _ = app_handle.emit("export-complete", ());
10381058
}
1039-
1040-
*app_handle
1041-
.state::<AppState>()
1042-
.export_task_handle
1043-
.lock()
1044-
.unwrap() = None;
10451059
});
10461060

10471061
*state.export_task_handle.lock().unwrap() = Some(task);

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod inpainting;
2727
mod lens_correction;
2828
mod lut_processing;
2929
mod mask_generation;
30+
mod multi_exposure;
3031
mod negative_conversion;
3132
mod panorama_stitching;
3233
mod panorama_utils;

src-tauri/src/multi_exposure.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
pub fn is_incamera_multiexposure_canon(file_bytes: &[u8]) -> bool {
2+
assert!(file_bytes.len() >= 8, "CR2 file must be at least 8 bytes");
3+
4+
match file_bytes.get(0..4) {
5+
Some([0x49, 0x49, 0x2A, 0x00]) => {}
6+
_ => return false,
7+
}
8+
9+
let walk = || -> Option<bool> {
10+
let b: [u8; 4] = file_bytes.get(4..8)?.try_into().ok()?;
11+
let ifd0_offset = u32::from_le_bytes(b) as usize;
12+
let exif_ifd_offset = _find_ifd_entry(file_bytes, ifd0_offset, 0x8769)? as usize;
13+
let maker_note_offset = _find_ifd_entry(file_bytes, exif_ifd_offset, 0x927C)? as usize;
14+
let multi_exp_block_offset =
15+
_find_ifd_entry(file_bytes, maker_note_offset, 0x4021)? as usize;
16+
let flag_offset = multi_exp_block_offset + 4;
17+
let v: [u8; 4] = file_bytes
18+
.get(flag_offset..flag_offset + 4)?
19+
.try_into()
20+
.ok()?;
21+
Some(u32::from_le_bytes(v) == 1)
22+
};
23+
24+
walk().unwrap_or(false)
25+
}
26+
27+
fn _find_ifd_entry(file_bytes: &[u8], ifd_offset: usize, tag_id: u16) -> Option<u32> {
28+
let rd16 = |offset: usize| -> Option<u16> {
29+
let b: [u8; 2] = file_bytes.get(offset..offset + 2)?.try_into().ok()?;
30+
Some(u16::from_le_bytes(b))
31+
};
32+
33+
let rd32 = |offset: usize| -> Option<u32> {
34+
let b: [u8; 4] = file_bytes.get(offset..offset + 4)?.try_into().ok()?;
35+
Some(u32::from_le_bytes(b))
36+
};
37+
38+
let entry_count = rd16(ifd_offset)? as usize;
39+
let capped_count = entry_count.min(512);
40+
41+
for i in 0..capped_count {
42+
let entry_offset = ifd_offset + 2 + i * 12;
43+
let tag = rd16(entry_offset)?;
44+
45+
if tag == tag_id {
46+
return rd32(entry_offset + 8);
47+
}
48+
}
49+
50+
None
51+
}
52+
53+
pub fn neutralize_wb_if_multiexposure(wb_coeffs: [f32; 4], file_bytes: &[u8]) -> [f32; 4] {
54+
if is_incamera_multiexposure_canon(file_bytes) {
55+
log::info!("[raw_hdr_wb] multi-exposure CR2 detected, neutralizing WB");
56+
let mut neutralized = wb_coeffs;
57+
for exp in &mut neutralized {
58+
if exp.is_finite() {
59+
*exp = 1.0;
60+
}
61+
}
62+
neutralized
63+
} else {
64+
wb_coeffs
65+
}
66+
}

src-tauri/src/raw_processing.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ fn develop_internal(
117117
developer.steps.retain(|&step| step != ProcessingStep::SRgb);
118118
}
119119

120+
raw_image.wb_coeffs =
121+
crate::multi_exposure::neutralize_wb_if_multiexposure(raw_image.wb_coeffs, file_bytes);
122+
120123
check_cancel()?;
121124
let mut developed_intermediate = developer.develop_intermediate(&raw_image)?;
122125

src/App.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import clsx from 'clsx';
88

99
import TitleBar from './window/TitleBar';
1010
import FolderTree from './components/panel/FolderTree';
11+
import SettingsPanel from './components/panel/SettingsPanel';
1112
import ExportPanel from './components/panel/right/ExportPanel';
1213
import Resizer from './components/ui/Resizer';
1314
import GlobalTooltip from './components/ui/GlobalTooltip';
@@ -107,6 +108,7 @@ function App() {
107108
rightPanelWidth,
108109
compactEditorPanelHeightOverride,
109110
activeRightPanel,
111+
isSettingsOpen,
110112
setUI,
111113
setRightPanel,
112114
} = useUIStore(
@@ -121,6 +123,7 @@ function App() {
121123
rightPanelWidth: state.rightPanelWidth,
122124
compactEditorPanelHeightOverride: state.compactEditorPanelHeightOverride,
123125
activeRightPanel: state.activeRightPanel,
126+
isSettingsOpen: state.isSettingsOpen,
124127
setUI: state.setUI,
125128
setRightPanel: state.setRightPanel,
126129
})),
@@ -709,6 +712,19 @@ function App() {
709712
requestThumbnails={requestThumbnails}
710713
/>
711714
)}
715+
{isSettingsOpen && appSettings && (
716+
<div className="absolute inset-0 z-50 flex bg-bg-secondary">
717+
<div className="w-full h-full flex flex-col p-8 lg:p-16 overflow-y-auto custom-scrollbar">
718+
<SettingsPanel
719+
appSettings={appSettings}
720+
onBack={() => setUI({ isSettingsOpen: false })}
721+
onLibraryRefresh={handleLibraryRefresh}
722+
onSettingsChange={handleSettingsChange}
723+
rootPaths={rootPaths}
724+
/>
725+
</div>
726+
</div>
727+
)}
712728
</div>
713729
{!selectedImage && isLibraryExportPanelVisible && (
714730
<Resizer direction={Orientation.Vertical} onMouseDown={createResizeHandler('right', rightPanelWidth)} />

src/components/panel/Editor.tsx

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import debounce from 'lodash.debounce';
88

99
import { ImageDimensions, useImageRenderSize } from '../../hooks/useImageRenderSize';
1010
import { Adjustments, AiPatch, MaskContainer } from '../../utils/adjustments';
11-
import { calculateCenteredCrop } from '../../utils/cropUtils';
11+
import { calculateCenteredCrop, rotateCropCenter } from '../../utils/cropUtils';
1212
import EditorToolbar from './editor/EditorToolbar';
1313
import ImageCanvas from './editor/ImageCanvas';
1414
import { Mask, SubMask } from './right/Masks';
@@ -1492,17 +1492,26 @@ export default function Editor({ onBackToLibrary, onContextMenu, transformWrappe
14921492
effectiveRotation,
14931493
);
14941494
} else {
1495-
if (!checkCropValid(currentAdjCrop, W, H, effectiveRotation)) {
1495+
const referenceRotation = prevCropParams.current?.rotation ?? rotation;
1496+
const rotationDelta = effectiveRotation - referenceRotation;
1497+
const followedCrop =
1498+
rotationChanged && rotationDelta !== 0
1499+
? rotateCropCenter(currentAdjCrop, W, H, rotationDelta)
1500+
: currentAdjCrop;
1501+
1502+
if (checkCropValid(followedCrop, W, H, effectiveRotation)) {
1503+
nextPixelCrop = followedCrop;
1504+
} else {
14961505
let low = 0.1;
14971506
let high = 1.0;
1498-
let bestCrop = currentAdjCrop;
1507+
let bestCrop = followedCrop;
14991508

15001509
for (let i = 0; i < 10; i++) {
15011510
let mid = (low + high) / 2;
1502-
let cx = currentAdjCrop.x + currentAdjCrop.width / 2;
1503-
let cy = currentAdjCrop.y + currentAdjCrop.height / 2;
1504-
let nw = currentAdjCrop.width * mid;
1505-
let nh = currentAdjCrop.height * mid;
1511+
let cx = followedCrop.x + followedCrop.width / 2;
1512+
let cy = followedCrop.y + followedCrop.height / 2;
1513+
let nw = followedCrop.width * mid;
1514+
let nh = followedCrop.height * mid;
15061515
let testCrop = {
15071516
unit: 'px' as const,
15081517
x: cx - nw / 2,

src/components/panel/MainLibrary.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
import { motion, AnimatePresence } from 'framer-motion';
1818
import { useTranslation } from 'react-i18next';
1919
import Button from '../ui/Button';
20-
import SettingsPanel from './SettingsPanel';
2120
import { ThemeProps, THEMES, DEFAULT_THEME_ID } from '../../utils/themes';
2221
import {
2322
AppSettings,
@@ -33,6 +32,7 @@ import { ImportState, Status } from '../ui/ExportImportProperties';
3332
import Text from '../ui/Text';
3433
import { TextColors, TextVariants, TextWeights } from '../../types/typography';
3534
import { useLibraryStore } from '../../store/useLibraryStore';
35+
import { useUIStore } from '../../store/useUIStore';
3636

3737
import LibraryGrid from './library/LibraryGrid';
3838
import { SearchInput, ViewOptionsDropdown } from './library/LibraryHeader';
@@ -89,7 +89,7 @@ export interface ColumnWidths {
8989

9090
export default function MainLibrary(props: MainLibraryProps) {
9191
const { t } = useTranslation();
92-
const [showSettings, setShowSettings] = useState(false);
92+
const setUI = useUIStore((state) => state.setUI);
9393
const [appVersion, setAppVersion] = useState('');
9494
const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
9595
const [latestVersion, setLatestVersion] = useState('');
@@ -263,16 +263,7 @@ export default function MainLibrary(props: MainLibraryProps) {
263263
</div>
264264

265265
<div className="w-full h-full flex flex-col p-8 lg:p-16 overflow-y-auto custom-scrollbar relative z-10">
266-
{showSettings ? (
267-
<SettingsPanel
268-
appSettings={props.appSettings}
269-
onBack={() => setShowSettings(false)}
270-
onLibraryRefresh={props.onLibraryRefresh}
271-
onSettingsChange={props.onSettingsChange}
272-
rootPaths={props.rootPaths}
273-
/>
274-
) : (
275-
<>
266+
<>
276267
<div className="my-auto text-left relative z-10">
277268
<Text variant={TextVariants.displayLarge}>{t('library.splash.brand')}</Text>
278269
<Text
@@ -320,7 +311,7 @@ export default function MainLibrary(props: MainLibraryProps) {
320311
</Button>
321312
<Button
322313
className="px-3 bg-surface text-text-primary shadow-md h-11"
323-
onClick={() => setShowSettings(true)}
314+
onClick={() => setUI({ isSettingsOpen: true })}
324315
size="lg"
325316
data-tooltip={t('settings.general.title')}
326317
variant="ghost"
@@ -401,7 +392,6 @@ export default function MainLibrary(props: MainLibraryProps) {
401392
)}
402393
</Text>
403394
</>
404-
)}
405395
</div>
406396
</div>
407397
</div>

0 commit comments

Comments
 (0)