Merge pull request #7121 from texjer/feat/camera-raw-sidecars

Camera Raw: Done saves the raws' settings to their sidecars without opening them
This commit is contained in:
David Mann authored and GitHub committed 2026-11-08 00:30:42 +01:00
commit 153fcff8c3
31 files changed
+835 -77

No files matched your search

+1 -1
View File
@@ -170,7 +170,7 @@ impl PhotocraftApp {
distort: Default::default(),
gradient: Default::default(),
camera_raw: None,
pending_raw_open: None,
raw_shelf: Vec::new(),
raw_redevelop: Vec::new(),
wide_angle: None,
dialog_edits: Default::default(),
@@ -30,6 +30,7 @@ use crate::widgets;
mod controls;
mod convert;
mod filmstrip;
mod menu;
mod open;
mod proxy;
@@ -38,6 +39,7 @@ mod sidecar;
pub use controls::show;
#[cfg(test)]
use convert::open_pixels;
pub use filmstrip::HeldRaw;
pub use menu::menu;
#[cfg(test)]
use open::{Coverage, commit_open_raw, document_index, finish_redevelop, open_raw, replace_raw_document};
@@ -1,4 +1,4 @@
use super::open::{Target, cancel, commit_open_raw, open_raw, raw_stage};
use super::open::{Target, cancel, raw_stage};
use super::sidecar::{load_settings, report_file_error, save_settings};
use super::{
Align2, BANDS, CameraRaw, CameraRawDialog, Color32, ERect, FontId, PANEL_W, PhotocraftApp, Pos2, Sense, Stroke, Tokens, ToneZone, Value, Wheel, curve_lut,
@@ -69,7 +69,7 @@ pub(super) fn commit(app: &mut PhotocraftApp) -> Result<Value, String> {
Target::OpenRaw => {
let raw = d.raw_open.clone().ok_or("no raw file is being opened")?;
let (params, original) = (d.params.clone(), d.original.clone());
commit_open_raw(app, &params, &raw, Some(&original))?
super::filmstrip::open_all(app, &params, &raw, &original)?
}
};
app.camera_raw = None;
@@ -188,10 +188,11 @@ fn curve_editor(ui: &mut egui::Ui, p: &mut CameraRaw, dirty: &mut bool, state: &
pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) {
if app.camera_raw.is_none()
&& let Some(raw) = app.pending_raw_open.take()
&& let Err(e) = open_raw(app, ctx, raw)
&& !app.raw_shelf.is_empty()
&& let Err(e) = super::filmstrip::open_held(app, ctx, 0)
{
// The document stays open, developed with the defaults.
app.raw_shelf.remove(0);
app.ui.status = e;
app.ui.status_error = true;
}
@@ -241,6 +242,7 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
let screen = ctx.content_rect();
let current_display = app.session.active().and_then(|st| app.session.color.canvas_display(&st.doc).ok());
let mut action: Option<&str> = None;
let mut show_held: Option<usize> = None;
egui::Area::new(egui::Id::unique("camera-raw-dialog")).order(egui::Order::Foreground).fixed_pos(screen.min).show(ctx, |ui| {
let Some(d) = app.camera_raw.as_mut() else { return };
history_keys(d, ctx);
@@ -278,7 +280,16 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
let animated_zoom = app.session.prefs().general.animated_zoom;
let scope = &mut app.ui.camera_raw_scope;
// Clip both pixels and scope overlays before painting the settings dock/footer.
let preview_body = ERect::from_min_max(body.min, pos2((body.right() - PANEL_W).max(body.left() + 1.0), body.bottom()));
// Several raws being opened: the filmstrip on the left.
let film_w = if d.target == Target::OpenRaw && app.raw_shelf.len() > 1 { super::filmstrip::FILM_W } else { 0.0 };
if film_w > 0.0 {
let film = ERect::from_min_max(body.min, pos2(body.left() + film_w - 1.0, body.bottom()));
painter.rect_filled(film, 0.0, t.dock);
painter.line_segment([film.right_top(), film.right_bottom()], Stroke::new(1.0, t.separator));
show_held = super::filmstrip::filmstrip(ui, film, &mut app.raw_shelf, d.raw_open.as_ref().map(|r| r.document), &t);
}
let preview_body =
ERect::from_min_max(pos2(body.left() + film_w, body.top()), pos2((body.right() - PANEL_W).max(body.left() + film_w + 1.0), body.bottom()));
let view = ERect::from_min_max(
preview_body.min + vec2(16.0, 16.0),
pos2((preview_body.right() - 16.0).max(preview_body.left() + 17.0), (preview_body.bottom() - 48.0).max(preview_body.top() + 17.0)),
@@ -476,6 +487,15 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
) {
action = Some(if role == widgets::ButtonRole::Default { "ok" } else { "cancel" });
}
// Done (open-time dialog): keep the settings in the raws' sidecars, open nothing.
if d.target == Target::OpenRaw
&& fu
.add_sized([90.0, 28.0], egui::Button::new(tl!("Done")))
.on_hover_text(tl!("Save the settings to each raw's sidecar without opening it"))
.clicked()
{
action = Some("done");
}
fu.add_space(12.0);
widgets::checkbox(&mut fu, &mut d.show_before, tl!("Before (Y)"));
fu.label(egui::RichText::new(format!("{:.0} ms", d.render_ms)).color(t.text_faint));
@@ -496,6 +516,12 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
}
});
crate::camera_raw_scope_ui::persist(app, ctx);
if let Some(i) = show_held
&& let Err(e) = super::filmstrip::show_raw(app, ctx, i)
{
app.ui.status = e;
app.ui.status_error = true;
}
if ctx.input(|i| i.key_pressed(egui::Key::Y))
&& let Some(d) = app.camera_raw.as_mut()
{
@@ -511,6 +537,9 @@ fn draw(app: &mut PhotocraftApp, ctx: &egui::Context, own_window: bool) {
}
}
Some("cancel") => cancel(app),
Some("done") => {
super::filmstrip::done(app);
}
Some("save") => {
let r = save_settings(app);
report_file_error(app, r);
@@ -48,24 +48,10 @@ pub(super) fn apply_raw_conversions(app: &mut PhotocraftApp, index: usize, d: &p
Ok(())
}
pub(super) fn open_pixels(
app: &mut PhotocraftApp,
ctx: &egui::Context,
layer: LayerId,
surf: photocraft_raster::Surface,
params: CameraRaw,
coverage: Coverage,
target: Target,
) -> Result<(), String> {
let st = app.session.active().ok_or("no document")?;
let canvas = st.doc.bounds();
let name = st.doc.layer(layer).map(|l| l.name.clone()).unwrap_or_default();
let mut params = params;
if params.encoding_gamma == 0.0 {
params.encoding_gamma = photocraft_engine::lens_cmds::encoding_gamma(&st.doc);
}
// Use the engine filter domain, including pixels outside the canvas: spatial Camera Raw
// stages (vignette, grain, local contrast) must agree with the committed result.
/// What the preview develops, and its size: the engine filter domain, including pixels outside the
/// canvas, since spatial Camera Raw stages (vignette, grain, local contrast) must agree with the
/// committed result.
pub(super) fn preview_area(canvas: Rect, surf: &photocraft_raster::Surface) -> Result<(Rect, usize, usize), String> {
let area = canvas.union(&surf.content_bounds());
let width = area.x1.checked_sub(area.x0).filter(|n| (1..=1_048_576).contains(n));
let height = area.y1.checked_sub(area.y0).filter(|n| (1..=1_048_576).contains(n));
@@ -76,6 +62,12 @@ pub(super) fn open_pixels(
if w.checked_mul(h).is_none_or(|pixels| pixels > 512_000_000) {
return Err("Camera Raw preview bounds are too large".into());
}
Ok((area, w, h))
}
/// `area` of `surf` (`w`×`h`) box-averaged down to at most [`PROXY_SIDE`] pixels on its long side:
/// the proxy, its size, and the side of the blocks averaged.
pub(super) fn box_proxy(surf: &photocraft_raster::Surface, area: Rect, w: usize, h: usize) -> (Vec<[f32; 4]>, usize, usize, usize) {
let k = w.max(h).div_ceil(PROXY_SIDE).max(1);
let (pw, ph) = (w.div_ceil(k), h.div_ceil(k));
let mut proxy = vec![[0.0f32; 4]; pw * ph];
@@ -98,6 +90,27 @@ pub(super) fn open_pixels(
proxy[py * pw + x] = [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]];
}
}
(proxy, pw, ph, k)
}
pub(super) fn open_pixels(
app: &mut PhotocraftApp,
ctx: &egui::Context,
layer: LayerId,
surf: photocraft_raster::Surface,
params: CameraRaw,
coverage: Coverage,
target: Target,
) -> Result<(), String> {
let st = app.session.active().ok_or("no document")?;
let canvas = st.doc.bounds();
let name = st.doc.layer(layer).map(|l| l.name.clone()).unwrap_or_default();
let mut params = params;
if params.encoding_gamma == 0.0 {
params.encoding_gamma = photocraft_engine::lens_cmds::encoding_gamma(&st.doc);
}
let (area, w, h) = preview_area(canvas, &surf)?;
let (proxy, pw, ph, k) = box_proxy(&surf, area, w, h);
// Coverage at the centre of each proxy block.
let sample = |value: &dyn Fn(i32, i32) -> f32| {
(0..ph)
@@ -0,0 +1,290 @@
//! The open-time dialog's filmstrip: raws opened together wait side by side in one Camera Raw
//! dialog, as in Photoshop's. Each keeps its own settings and demosaic choice when another is
//! shown, Open develops every one and Cancel closes every one.
use super::open::{Target, commit_open_raw, open_raw_with, return_on_failure};
use super::sidecar::read_sidecar;
use super::{
Align2, CameraRaw, CameraRawDialog, Color32, ERect, FontId, PhotocraftApp, RawOpen, Rect, Sense, Stroke, Tokens, Value, json, pos2, vec2, widgets,
};
/// The filmstrip column, shown when more than one raw is being opened.
pub(super) const FILM_W: f32 = 150.0;
/// Long side of a filmstrip thumbnail, in pixels.
const THUMB_SIDE: usize = 240;
/// A raw waiting in the open-time dialog: opened (developed with the defaults) and reviewed
/// before Open keeps it.
pub struct HeldRaw {
pub raw: RawOpen,
/// Its settings and the settings it started from (its sidecar's, or the defaults) once it
/// has been shown and left for another (`None`: not edited yet).
pub(super) kept: Option<(CameraRaw, CameraRaw)>,
/// The filmstrip thumbnail until it is first drawn, then its texture.
pub(super) thumb: Option<egui::ColorImage>,
tex: Option<egui::TextureHandle>,
}
impl HeldRaw {
pub(super) fn new(app: &PhotocraftApp, raw: RawOpen) -> Self {
let thumb = super::open::document_index(app, raw.document).and_then(|i| thumbnail(app, i));
HeldRaw { raw, kept: None, thumb, tex: None }
}
}
/// A filmstrip thumbnail of document `index` (a just-developed raw: its first pixel layer).
fn thumbnail(app: &PhotocraftApp, index: usize) -> Option<egui::ColorImage> {
let doc = &app.session.documents().get(index)?.doc;
let surf = doc.layers.iter().find_map(|l| l.surface())?;
let area = doc.bounds();
let (w, h) = (area.width() as usize, area.height() as usize);
if w == 0 || h == 0 {
return None;
}
let k = w.max(h).div_ceil(THUMB_SIDE).max(1);
let (tw, th) = (w.div_ceil(k), h.div_ceil(k));
let mut acc = vec![[0.0f32; 5]; tw * th];
let mut row = vec![[0.0f32; 4]; w];
for y in (0..h).step_by(k.div_ceil(2).max(1)) {
surf.read_rgba_into(Rect::new(area.x0, area.y0 + y as i32, area.x1, area.y0 + y as i32 + 1), &mut row);
for (x, q) in row.iter().enumerate().step_by(k.div_ceil(2).max(1)) {
let a = &mut acc[(y / k) * tw + x / k];
for c in 0..4 {
a[c] += q[c];
}
a[4] += 1.0;
}
}
let px: Vec<[f32; 4]> = acc.iter().map(|a| if a[4] > 0.0 { [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]] } else { [0.0; 4] }).collect();
let display = app.session.color.canvas_display(doc).ok()?;
Some(CameraRawDialog::image(&px, tw, th, &display))
}
/// `px` (`w`×`h`) box-averaged down to at most [`THUMB_SIDE`] pixels on its long side.
fn shrink(px: &[[f32; 4]], w: usize, h: usize) -> (Vec<[f32; 4]>, usize, usize) {
let k = w.max(h).div_ceil(THUMB_SIDE).max(1);
let (tw, th) = (w.div_ceil(k), h.div_ceil(k));
let mut acc = vec![[0.0f32; 5]; tw * th];
for (i, q) in px.iter().enumerate().take(w * h) {
if let Some(a) = acc.get_mut((i / w / k) * tw + (i % w) / k) {
for c in 0..4 {
a[c] += q[c];
}
a[4] += 1.0;
}
}
(acc.iter().map(|a| if a[4] > 0.0 { [a[0] / a[4], a[1] / a[4], a[2] / a[4], a[3] / a[4]] } else { [0.0; 4] }).collect(), tw, th)
}
/// Shows raw `i` of the filmstrip, with the settings it was left with (its sidecar's, the
/// Camera Raw Defaults' Auto, or the defaults the first time).
pub(super) fn open_held(app: &mut PhotocraftApp, ctx: &egui::Context, i: usize) -> Result<(), String> {
let held = app.raw_shelf.get(i).ok_or("no raw file at that place in the filmstrip")?;
let (raw, kept) = (held.raw.clone(), held.kept.clone());
open_raw_with(app, ctx, raw, kept)
}
/// Shows raw `to` of the filmstrip; the one being left keeps its settings, and its thumbnail
/// shows them.
pub(super) fn show_raw(app: &mut PhotocraftApp, ctx: &egui::Context, to: usize) -> Result<(), String> {
let Some(d) = app.camera_raw.as_ref() else { return Ok(()) };
let current = d.raw_open.as_ref().map(|r| r.document);
if d.target != Target::OpenRaw || app.raw_shelf.get(to).is_none_or(|h| Some(h.raw.document) == current) {
return Ok(());
}
keep_settings(app);
// On success this replaces the dialog. On failure the dialog stays on the raw being left, and
// the other keeps its place in the filmstrip.
let active = app.session.active_index();
let shown = open_held(app, ctx, to);
if shown.is_err()
&& let Some(i) = active
{
app.session.set_active(i);
}
shown
}
/// Stores the dialog's settings (and demosaic choice) on its filmstrip raw, and refreshes the
/// raw's thumbnail from the preview.
pub(super) fn keep_settings(app: &mut PhotocraftApp) {
let Some(d) = app.camera_raw.as_ref() else { return };
let Some(raw) = &d.raw_open else { return };
let Some(held) = app.raw_shelf.iter_mut().find(|h| h.raw.document == raw.document) else { return };
held.kept = Some((d.params.clone(), d.original.clone()));
held.raw.demosaic = raw.demosaic;
// The preview may not have caught up with the last edit: the thumbnail shows what was kept.
let frame = (d.dirty || d.render_pending()).then(|| d.proxy_job().run().px);
let developed = frame.as_deref().unwrap_or(&d.processed);
if !developed.is_empty() {
let (px, w, h) = shrink(developed, d.pw, d.ph);
held.thumb = Some(CameraRawDialog::image(&px, w, h, &d.display));
held.tex = None;
}
}
/// The settings Open develops a never-shown filmstrip raw with, and what they started from:
/// its sidecar's, else the Camera Raw Defaults' Auto when that is on, else the defaults.
pub(super) fn first_settings(app: &PhotocraftApp, raw: &RawOpen) -> (CameraRaw, Option<CameraRaw>) {
if let Some((loaded, _, _)) = raw.path.as_deref().and_then(read_sidecar) {
return (loaded.clone(), Some(loaded));
}
let mut p = CameraRaw::default();
// From the proxy the dialog would use, so showing a raw doesn't change what Auto gives it.
if app.session.prefs().raw_defaults.apply_auto_tone
&& let Some(st) = super::open::document_index(app, raw.document).and_then(|i| app.session.documents().get(i))
&& let Some(surf) = st.doc.layers.iter().find_map(|l| l.surface())
&& let Ok((area, w, h)) = super::convert::preview_area(st.doc.bounds(), surf)
{
let (proxy, ..) = super::convert::box_proxy(surf, area, w, h);
photocraft_algo::camera_raw::auto_tone(&proxy, &mut p);
}
(p, None)
}
/// Open with raws in the filmstrip: the shown one as the dialog has it, then every other with
/// its own settings. One reply for one raw, `{"opened": [...]}` for several. A raw leaves the
/// filmstrip once it has opened or its background develop has started: one that fails stays or
/// comes back there with its settings, so the dialog returns for it (or Cancel still closes it).
pub(super) fn open_all(app: &mut PhotocraftApp, params: &CameraRaw, raw: &RawOpen, original: &CameraRaw) -> Result<Value, String> {
keep_settings(app);
let others: Vec<photocraft_doc::DocId> = app.raw_shelf.iter().map(|h| h.raw.document).filter(|id| *id != raw.document).collect();
let first = commit_open_raw(app, params, raw, Some(original));
if let Ok(v) = &first {
return_on_failure(app, v, original);
app.raw_shelf.retain(|h| h.raw.document != raw.document);
}
if others.is_empty() {
return first;
}
let mut opened = Vec::new();
let mut failed = Vec::new();
match first {
Ok(v) => opened.push(v),
Err(e) => failed.push(format!("{}: {e}", raw.name)),
}
for id in others {
let Some(held) = app.raw_shelf.iter().find(|h| h.raw.document == id) else { continue };
let (held_raw, kept) = (held.raw.clone(), held.kept.clone());
let (p, loaded) = match kept {
Some((p, original)) => (p, Some(original)),
None => first_settings(app, &held_raw),
};
match commit_open_raw(app, &p, &held_raw, loaded.as_ref()) {
Ok(v) => {
return_on_failure(app, &v, loaded.as_ref().unwrap_or(&CameraRaw::default()));
opened.push(v);
app.raw_shelf.retain(|h| h.raw.document != id);
}
Err(e) => failed.push(format!("{}: {e}", held_raw.name)),
}
}
if !failed.is_empty() {
let e = failed.join("; ");
if opened.is_empty() {
return Err(e);
}
crate::notices::error(app, e);
}
Ok(json!({"opened": opened}))
}
/// Done: saves each raw's settings to its sidecar, as Camera Raw's Done does, and opens none of
/// them (they close again).
pub(super) fn done(app: &mut PhotocraftApp) -> Value {
use super::sidecar::Stored;
keep_settings(app);
let Some(d) = app.camera_raw.as_ref() else { return Value::Null };
let Some(raw) = d.raw_open.clone() else { return Value::Null };
let mut raws = vec![(raw.clone(), d.params.clone())];
for held in app.raw_shelf.iter().filter(|h| h.raw.document != raw.document) {
let p = match &held.kept {
Some((p, _)) => p.clone(),
None => first_settings(app, &held.raw).0,
};
raws.push((held.raw.clone(), p));
}
let stored: Vec<Stored> = raws.iter().map(|(raw, p)| super::sidecar::store_sidecar(app, raw, p)).collect();
let saved: Vec<&String> = stored.iter().filter_map(|s| if let Stored::Written(name) = s { Some(name) } else { None }).collect();
// A failed write (already in a notice) keeps the dialog and every raw open, so no edit is lost.
if stored.contains(&Stored::Failed) {
app.ui.status = tl!("Some settings could not be saved; the raws stay open").to_string();
app.ui.status_error = true;
return json!({"done": false, "saved": saved});
}
app.ui.status = if !saved.is_empty() {
crate::i18n::fmt(tl!("Saved Camera Raw settings to {file}"), &[("file", &saved.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "))])
} else if stored.iter().all(|s| *s == Stored::Defaults) {
tl!("Nothing to save: these raws are at their default settings").to_string()
} else {
tl!("No settings saved: these raws can't keep a sidecar here").to_string()
};
app.ui.status_error = saved.is_empty() && stored.contains(&Stored::Skipped);
let saved = json!(saved);
app.camera_raw = None;
app.raw_shelf.clear();
for (raw, _) in &raws {
if let Some(index) = super::open::document_index(app, raw.document) {
app.session.close(index);
}
}
app.sync_views();
json!({"done": true, "saved": saved})
}
/// The filmstrip of raws being opened: a thumbnail and name each, the shown one outlined, a dot
/// on edited ones. Returns the one clicked.
pub(super) fn filmstrip(ui: &mut egui::Ui, rect: ERect, shelf: &mut [HeldRaw], current: Option<photocraft_doc::DocId>, t: &Tokens) -> Option<usize> {
let mut clicked = None;
let mut film = ui.new_child(egui::UiBuilder::new().max_rect(rect.shrink2(vec2(6.0, 6.0))));
film.set_clip_rect(rect.intersect(ui.clip_rect()));
egui::ScrollArea::vertical().id_salt("camera-raw-filmstrip").auto_shrink([false, false]).show(&mut film, |ui| {
ui.spacing_mut().item_spacing.y = 6.0;
let w = ui.available_width();
for (i, held) in shelf.iter_mut().enumerate() {
if held.tex.is_none()
&& let Some(img) = held.thumb.take()
{
held.tex = Some(ui.ctx().load_texture(format!("camera-raw-thumb-{}", held.raw.document.0), img, egui::TextureOptions::LINEAR));
}
let (tile, response) = ui.allocate_exact_size(vec2(w, w * 0.75 + 18.0), Sense::click());
let frame = ERect::from_min_size(tile.min, vec2(w, w * 0.75));
let p = ui.painter();
p.rect_filled(frame, 2.0, if response.hovered() { t.hover } else { t.canvas });
if let Some(tex) = &held.tex {
let size = tex.size_vec2();
let fit = ((frame.width() - 8.0) / size.x.max(1.0)).min((frame.height() - 8.0) / size.y.max(1.0));
let r = ERect::from_center_size(frame.center(), size * fit);
widgets::checker(p, r, 6.0);
p.image(tex.id(), r, ERect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
}
if Some(held.raw.document) == current {
p.rect_stroke(frame, 2.0, Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
}
if held.kept.as_ref().is_some_and(|(p, _)| !p.is_identity()) {
p.circle_filled(frame.right_top() + vec2(-7.0, 7.0), 3.0, t.text);
}
let name = ERect::from_min_max(pos2(tile.left(), frame.bottom()), tile.max);
let font = FontId::proportional(10.5);
let label = tail_fitting(p, &held.raw.name, font.clone(), name.width());
p.with_clip_rect(name).text(name.center(), Align2::CENTER_CENTER, label, font, t.text_dim);
let response = response.on_hover_text(&held.raw.name);
response.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, Some(held.raw.document) == current, &held.raw.name));
if response.clicked() {
clicked = Some(i);
}
}
});
clicked
}
/// `name`, or "…" and as much of its end as fits in `width`: raws from one shoot differ at the
/// end of their names (the frame number).
fn tail_fitting(p: &egui::Painter, name: &str, font: FontId, width: f32) -> String {
let fits = |s: &str| p.layout_no_wrap(s.to_owned(), font.clone(), Color32::WHITE).size().x <= width;
if fits(name) {
return name.to_owned();
}
let chars: Vec<char> = name.chars().collect();
(1..chars.len()).map(|start| format!("…{}", chars[start..].iter().collect::<String>())).find(|s| fits(s)).unwrap_or_default()
}
@@ -149,7 +149,7 @@ fn queue_identity_targets_an_inactive_raw_without_changing_the_active_document()
app.session.set_active(0);
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id);
queue_open_dialog(&mut app, raw.document, &raw.name, raw.path.as_deref(), raw.bytes.as_deref().map(Vec::as_slice), &[]).unwrap();
let queued = app.pending_raw_open.as_ref().expect("the explicitly named RAW was queued");
let queued = app.raw_shelf.first().map(|h| &h.raw).expect("the explicitly named RAW was queued");
assert_eq!(queued.document, raw.document);
assert_eq!((&queued.name, &queued.path, &queued.bytes), (&raw.name, &raw.path, &raw.bytes));
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id, "queueing does not activate the target");
@@ -160,12 +160,12 @@ fn queue_identity_targets_an_inactive_raw_without_changing_the_active_document()
fn queue_identity_rejects_a_missing_document_without_replacing_the_pending_review() {
let (mut app, raw, control) = identity_app();
queue_open_dialog(&mut app, raw.document, &raw.name, raw.path.as_deref(), raw.bytes.as_deref().map(Vec::as_slice), &[]).unwrap();
let pending = app.pending_raw_open.as_ref().unwrap().clone();
let pending = app.raw_shelf.first().map(|h| &h.raw).unwrap().clone();
app.session.set_active(0);
let missing = photocraft_doc::DocId::fresh();
assert!(!app.session.documents().iter().any(|state| state.doc.id == missing));
assert!(queue_open_dialog(&mut app, missing, "missing.dng", Some("missing.dng"), Some(&[1, 2, 3]), &[]).is_err());
let queued = app.pending_raw_open.as_ref().expect("the existing review is preserved");
let queued = app.raw_shelf.first().map(|h| &h.raw).expect("the existing review is preserved");
assert_eq!(queued.document, pending.document);
assert_eq!((&queued.name, &queued.path, &queued.bytes), (&pending.name, &pending.path, &pending.bytes));
assert_eq!(app.session.active().unwrap().doc.id, control.doc.id);
@@ -35,15 +35,16 @@ pub fn menu(app: &mut PhotocraftApp, ctx: &egui::Context, id: &str, params: &Val
for (key, value) in fields {
match key.as_str() {
"set" | "scope" | "view" | "demosaic" => {}
"before" | "commit" | "cancel" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" if value.is_boolean() => {}
"before" | "commit" | "cancel" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" => {
"before" | "commit" | "cancel" | "done" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" if value.is_boolean() => {}
"before" | "commit" | "cancel" | "done" | "saveSettings" | "loadSettings" | "undo" | "redo" | "auto" => {
return Some(Err(format!("Camera Raw {key} must be a boolean")));
}
_ => return Some(Err(format!("unknown Camera Raw ui property {key}"))),
}
}
if ui.get("commit").and_then(Value::as_bool) == Some(true) && ui.get("cancel").and_then(Value::as_bool) == Some(true) {
return Some(Err("Camera Raw cannot commit and cancel together".into()));
let ends = ["commit", "cancel", "done"].into_iter().filter(|k| ui.get(*k).and_then(Value::as_bool) == Some(true)).count();
if ends > 1 {
return Some(Err("Camera Raw takes one of commit, cancel and done".into()));
}
for (key, run) in [("saveSettings", save_settings as fn(&mut PhotocraftApp) -> Result<Value, String>), ("loadSettings", load_settings)] {
if ui.get(key).and_then(Value::as_bool) == Some(true) {
@@ -72,6 +73,9 @@ fn menu_update(app: &mut PhotocraftApp, ctx: &egui::Context, ui: &Value) -> Opti
// Stage both filter and presentation updates. No texture, preference or dialog mutation
// occurs until every part of the request has passed validation.
let d = app.camera_raw.as_ref()?;
if ui.get("done").and_then(Value::as_bool) == Some(true) && d.target != super::open::Target::OpenRaw {
return Some(Err("Done is for raws being opened".into()));
}
let mut next_params = d.params.clone();
// Auto (the Light panel's button): the tone sliders from the image's histogram.
if ui.get("auto").and_then(Value::as_bool) == Some(true) {
@@ -165,5 +169,8 @@ fn menu_update(app: &mut PhotocraftApp, ctx: &egui::Context, ui: &Value) -> Opti
if ui.get("commit").and_then(Value::as_bool) == Some(true) {
return Some(commit(app));
}
if ui.get("done").and_then(Value::as_bool) == Some(true) {
return Some(Ok(super::filmstrip::done(app)));
}
Some(Ok(app.camera_raw.as_ref().map(|d| d.describe(&app.ui.camera_raw_scope, &app.ui.camera_raw_preview)).unwrap_or(Value::Null)))
}
@@ -45,7 +45,8 @@ pub fn without_develop_note(warnings: &[String]) -> Vec<String> {
}
/// Queues the open-time dialog for the document whose import completed, independent of the
/// active tab. A delayed completion for a closed document must not review another photograph.
/// active tab: the dialog shows on the next frame, or adds it to its filmstrip when it is already
/// open. A delayed completion for a closed document must not review another photograph.
/// `warnings` are the import's: its note says which demosaic method the open developed with.
pub fn queue_open_dialog(
app: &mut PhotocraftApp,
@@ -61,22 +62,42 @@ pub fn queue_open_dialog(
// The method the open actually used (`jobs::import_options` at the job's start), from its
// note: the preference may have changed while a background open ran.
let method = photocraft_io::raw::developed_demosaic(warnings).unwrap_or_else(|| app.session.prefs().raw_defaults.demosaic.method());
app.pending_raw_open = Some(RawOpen {
if app.raw_shelf.iter().any(|h| h.raw.document == document) {
return Ok(());
}
let raw = RawOpen {
document,
name: name.to_string(),
path: path.map(str::to_string),
bytes: bytes.map(|b| std::sync::Arc::new(b.to_vec())),
developed_with: method,
demosaic: method,
});
};
let held = super::filmstrip::HeldRaw::new(app, raw);
app.raw_shelf.push(held);
Ok(())
}
/// Opens the queued open-time dialog on its document's (only) layer.
/// Opens the open-time dialog on a queued raw's (only) layer.
#[cfg(test)]
pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpen) -> Result<(), String> {
open_raw_with(app, ctx, raw, None)
}
/// [`open_raw`], or with `kept` settings and the settings they started from (a filmstrip raw
/// shown again).
pub(super) fn open_raw_with(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpen, kept: Option<(CameraRaw, CameraRaw)>) -> Result<(), String> {
let index = document_index(app, raw.document).ok_or("the raw document was closed")?;
app.session.set_active(index);
let (layer, surf, _) = crate::distort_ui::active_pixels(app)?;
if let Some((params, original)) = kept {
open_pixels(app, ctx, layer, surf, params, Coverage::None, Target::OpenRaw)?;
if let Some(d) = app.camera_raw.as_mut() {
d.original = original;
}
attach_raw(app, raw);
return Ok(());
}
// The file's sidecar (`IMG_0001.xmp` beside it) fills the dialog, as Camera Raw's does; the
// status bar names what it could not apply.
let mut params = CameraRaw::default();
@@ -103,6 +124,12 @@ pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpe
photocraft_algo::camera_raw::auto_tone(&d.proxy, &mut d.params);
d.dirty = true;
}
attach_raw(app, raw);
Ok(())
}
/// Gives the open dialog its raw: the file name, and the camera tone curve it was developed with.
fn attach_raw(app: &mut PhotocraftApp, raw: RawOpen) {
let curve = match (&raw.bytes, &raw.path) {
(Some(b), _) => Some(photocraft_io::raw::tone_curve(b)),
#[cfg(not(target_arch = "wasm32"))]
@@ -117,7 +144,6 @@ pub(super) fn open_raw(app: &mut PhotocraftApp, ctx: &egui::Context, raw: RawOpe
d.raw_curve = curve;
d.raw_open = Some(raw);
}
Ok(())
}
pub(super) fn document_index(app: &PhotocraftApp, id: photocraft_doc::DocId) -> Option<usize> {
@@ -153,6 +179,9 @@ pub struct Redevelop {
filter_step: bool,
/// The Camera Raw Defaults the open started with, for the conversions after the filter step.
defaults: photocraft_engine::prefs::RawDefaults,
/// The dialog's Open started this, from these settings: if the re-develop fails, the raw goes
/// back to the filmstrip to be tried again or cancelled.
back_to_filmstrip: Option<CameraRaw>,
}
/// The sliders a raw open applies while developing, before the tone curve (as Camera Raw does).
@@ -225,7 +254,7 @@ pub(super) fn commit_open_raw(app: &mut PhotocraftApp, params: &CameraRaw, raw:
}
photocraft_engine::jobs::Started::Job(job) => {
let defaults = app.session.prefs().raw_defaults.clone();
app.raw_redevelop.push(Redevelop { job, params: params.clone(), raw: raw.clone(), filter_step: false, defaults });
app.raw_redevelop.push(Redevelop { job, params: params.clone(), raw: raw.clone(), filter_step: false, defaults, back_to_filmstrip: None });
app.ui.status = label;
app.ui.status_error = false;
Ok(json!({"job": job.0, "pending": true}))
@@ -233,6 +262,15 @@ pub(super) fn commit_open_raw(app: &mut PhotocraftApp, params: &CameraRaw, raw:
}
}
/// `reply` is [`commit_open_raw`]'s from the dialog's Open: if it started a background re-develop
/// and that fails, the raw comes back to the filmstrip, its settings having started from `original`.
pub(super) fn return_on_failure(app: &mut PhotocraftApp, reply: &Value, original: &CameraRaw) {
let job = reply.get("job").and_then(Value::as_u64);
if let Some(pending) = app.raw_redevelop.iter_mut().find(|r| Some(r.job.0) == job && !r.filter_step) {
pending.back_to_filmstrip = Some(original.clone());
}
}
/// Swap the default-developed document `doc_id` for the re-developed one, at the same tab
/// position and with the same path. Returns `{document, docId, previous, warnings, wb}`:
/// `document` is the tab index at apply time, `docId` is the replacement's stable identity.
@@ -324,7 +362,7 @@ fn finish_open_raw(
&& let Some(job) = r.get("job").and_then(Value::as_u64)
{
let job = photocraft_engine::jobs::JobId(job);
app.raw_redevelop.push(Redevelop { job, params: rest.clone(), raw: raw.clone(), filter_step: true, defaults: d.clone() });
app.raw_redevelop.push(Redevelop { job, params: rest.clone(), raw: raw.clone(), filter_step: true, defaults: d.clone(), back_to_filmstrip: None });
result["filter"] = r;
mark_unmodified(app, index);
return Ok(result);
@@ -373,7 +411,7 @@ pub fn opened_without_dialog(app: &mut PhotocraftApp, index: usize, name: &str,
/// Every k-th pixel of a surface's content, k chosen so the longer side has about `max_side`
/// samples: enough for a histogram (Auto tone) without reading a whole raw.
fn sample_pixels(surf: &photocraft_raster::Surface, max_side: usize) -> Vec<[f32; 4]> {
pub(super) fn sample_pixels(surf: &photocraft_raster::Surface, max_side: usize) -> Vec<[f32; 4]> {
let area = surf.content_bounds();
let (w, h) = (area.x1.saturating_sub(area.x0).max(0) as usize, area.y1.saturating_sub(area.y0).max(0) as usize);
if w == 0 || h == 0 {
@@ -435,7 +473,15 @@ pub fn on_redevelop_event(app: &mut PhotocraftApp, e: &photocraft_engine::jobs::
Err(err) => crate::notices::error(app, format!("{}: {err}", e.label)),
},
photocraft_engine::jobs::JobOutcome::Failed(err) => {
// The document stays open, developed with the defaults.
// The document stays open, developed with the defaults; from the dialog, it goes back
// to the filmstrip with its settings, which may have no sidecar to keep them.
if let Some(original) = pending.back_to_filmstrip
&& document_index(app, pending.raw.document).is_some()
{
let mut held = super::filmstrip::HeldRaw::new(app, pending.raw);
held.kept = Some((pending.params, original));
app.raw_shelf.push(held);
}
app.sync_views();
crate::notices::error(app, format!("{}: {err}", e.label));
}
@@ -447,13 +493,18 @@ pub fn on_redevelop_event(app: &mut PhotocraftApp, e: &photocraft_engine::jobs::
true
}
/// Cancel: closes the dialog; for the open-time dialog also the document it was opening.
/// Cancel: closes the dialog; for the open-time dialog also the documents it was opening (every
/// raw in its filmstrip).
pub(super) fn cancel(app: &mut PhotocraftApp) {
let Some(d) = app.camera_raw.take() else { return };
if let (Target::OpenRaw, Some(raw)) = (d.target, &d.raw_open)
&& let Some(index) = document_index(app, raw.document)
{
app.session.close(index);
if let (Target::OpenRaw, Some(raw)) = (d.target, &d.raw_open) {
let mut opening: Vec<_> = std::mem::take(&mut app.raw_shelf).into_iter().map(|h| h.raw.document).collect();
opening.push(raw.document);
for id in opening {
if let Some(index) = document_index(app, id) {
app.session.close(index);
}
}
app.sync_views();
app.ui.status = crate::i18n::fmt(tl!("Canceled opening {name}"), &[("name", &raw.name)]);
app.ui.status_error = false;
@@ -52,27 +52,49 @@ pub(super) fn write_sidecar(app: &mut PhotocraftApp, raw: &RawOpen, params: &Cam
if !app.session.prefs().raw_defaults.write_xmp_sidecars || loaded == Some(params) {
return;
}
store_sidecar(app, raw, params);
}
/// What [`store_sidecar`] did.
#[derive(Debug, PartialEq)]
pub(super) enum Stored {
/// Written: the sidecar's name.
Written(String),
/// Nothing to keep: default settings and no sidecar of ours to update.
Defaults,
/// This raw can't keep a sidecar here: an automation request, a DNG or a raw without a path,
/// another application's sidecar, or no file writer.
Skipped,
/// The write failed (already reported in a notice).
Failed,
}
/// Writes `params` to `raw`'s sidecar under [`write_sidecar`]'s rules, whatever the preference
/// (Done asks for it).
pub(super) fn store_sidecar(app: &mut PhotocraftApp, raw: &RawOpen, params: &CameraRaw) -> Stored {
if app.automation_input || app.session.authorize.is_some() {
log::info!("{}: sidecar not written for an automation request", raw.name);
return;
return Stored::Skipped;
}
let Some(path) = raw.path.as_deref().filter(|p| !is_dng(p)) else { return };
let Some(path) = raw.path.as_deref().filter(|p| !is_dng(p)) else { return Stored::Skipped };
let sidecar = sidecar_path(path);
let name = crate::file_open::display_name(&sidecar);
match existing_sidecar_is_ours(&sidecar) {
Some(false) => {
let kept = crate::i18n::fmt(tl!("Kept {name}: written by another application"), &[("name", &name)]);
crate::notices::post(app, kept, Vec::new(), false, None);
return;
return Stored::Skipped;
}
None if params.is_identity() => return,
None if params.is_identity() => return Stored::Defaults,
_ => {}
}
let bytes = photocraft_io::camera_raw_xmp::save(params).into_bytes();
let Some(write) = app.services.write.as_mut() else { return };
let Some(write) = app.services.write.as_mut() else { return Stored::Skipped };
if let Err(e) = write(&sidecar, &bytes) {
crate::notices::error(app, crate::i18n::fmt(tl!("Could not write {name}: {error}"), &[("name", &name), ("error", &e)]));
return Stored::Failed;
}
Stored::Written(name)
}
/// `None` when there is no sidecar at `sidecar`, else whether PhotoCraft wrote it. One that
+268 -24
View File
@@ -1,5 +1,10 @@
use super::*;
/// The first raw waiting for the open-time dialog, taken off its filmstrip.
fn take_queued(app: &mut PhotocraftApp) -> Option<RawOpen> {
(!app.raw_shelf.is_empty()).then(|| app.raw_shelf.remove(0).raw)
}
/// #9550: a Color Mixer Hue track runs through the neighbouring bands' hues (Reds: magenta to
/// orange, wrapping round), not from grey; Saturation and Luminance keep grey to the colour.
#[test]
@@ -358,7 +363,7 @@ fn raw_opens_in_camera_raw_and_open_redevelops() {
// A raw has no embedded profile (ProPhoto RGB is the space it is developed into), so Ask
// When Opening (on by default) has nothing to ask about.
assert!(app.ui.dialogs.is_empty(), "no profile prompt for a raw");
let raw = app.pending_raw_open.take().expect("a raw open queues the dialog");
let raw = take_queued(&mut app).expect("a raw open queues the dialog");
open_raw(&mut app, &ctx, raw).unwrap();
let d = app.camera_raw.as_ref().unwrap();
assert_eq!(d.target, Target::OpenRaw);
@@ -388,7 +393,7 @@ fn the_demosaic_method_is_chosen_at_open() {
let ctx = egui::Context::default();
app.open_bytes("IMG_0002.dng", &dng).unwrap();
let ahd = pixels(&app);
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let d = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"demosaic": "bilinear"}})).unwrap().unwrap();
assert_eq!(d["openingRaw"]["demosaic"], "bilinear");
@@ -405,7 +410,7 @@ fn the_demosaic_method_is_chosen_at_open() {
app.run("prefs.set", json!({"path": "rawDefaults.demosaic", "value": "bilinear"})).unwrap();
app.open_bytes("IMG_0003.dng", &dng).unwrap();
assert_eq!(pixels(&app), bilinear, "developed with the preference's method");
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let d = app.camera_raw.as_ref().unwrap().describe(&app.ui.camera_raw_scope, &app.ui.camera_raw_preview);
assert_eq!(d["openingRaw"]["demosaic"], "bilinear");
@@ -419,11 +424,11 @@ fn the_demosaic_method_is_chosen_at_open() {
// The preference may change while a background open runs: the dialog starts from the
// method the open developed with (its note), not from the newer preference.
let warnings = app.open_bytes("IMG_0004.dng", &dng).unwrap();
app.pending_raw_open.take();
take_queued(&mut app);
app.run("prefs.set", json!({"path": "rawDefaults.demosaic", "value": "ahd"})).unwrap();
let document = app.session.active().unwrap().doc.id;
queue_open_dialog(&mut app, document, "IMG_0004.dng", None, Some(&dng), &warnings).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
use photocraft_io::raw::Demosaic;
assert_eq!((raw.developed_with, raw.demosaic), (Demosaic::Bilinear, Demosaic::Bilinear));
}
@@ -433,7 +438,7 @@ fn shadows_of_a_raw_open_act_on_the_raw_data_and_the_preview_shows_it() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("IMG_0002.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
assert!(app.camera_raw.as_ref().unwrap().raw_curve.is_some(), "the preview knows how the raw was developed");
let mean = |px: &[[f32; 4]]| px.iter().map(|p| p[0] + p[1] + p[2]).sum::<f32>() / px.len() as f32;
@@ -454,7 +459,7 @@ fn clarity_texture_and_dehaze_of_a_raw_open_act_on_the_raw_data() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("IMG_0003.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let before = mean_red(&app);
let preview_before = app.camera_raw.as_ref().unwrap().processed.clone();
@@ -476,7 +481,7 @@ fn background_redevelop_keeps_the_tab_position_and_view() {
app.open_bytes("mid.dng", &dng).unwrap();
app.run("file.new", json!({"width": 12, "height": 12})).unwrap();
app.sync_views();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
let old_id = raw.document;
assert_eq!(document_index(&app, old_id), Some(1));
app.ui.views[1].zoom = 3.0;
@@ -516,7 +521,7 @@ fn a_raw_opens_and_redevelops_at_the_raw_defaults_resolution() {
app.run("prefs.set", json!({"path": "rawDefaults.resolution", "value": 240})).unwrap();
app.open_bytes("IMG_0004.dng", &dng).unwrap();
assert_eq!(app.session.active().unwrap().doc.resolution_dpi, 240.0);
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
@@ -526,19 +531,201 @@ fn a_raw_opens_and_redevelops_at_the_raw_defaults_resolution() {
assert_eq!(app.session.active().unwrap().doc.resolution_dpi, 240.0, "automation open");
}
/// Raws opened together share the dialog in a filmstrip, each with its own settings; Open
/// develops them all, Cancel closes them all.
#[test]
fn several_raws_share_the_dialog_and_keep_their_own_settings() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("one.dng", &dng).unwrap();
app.open_bytes("two.dng", &dng).unwrap();
app.open_bytes("three.dng", &dng).unwrap();
assert_eq!(app.raw_shelf.len(), 3);
assert!(app.raw_shelf.iter().all(|h| h.thumb.is_some()), "each has a thumbnail");
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
app.camera_raw.as_mut().unwrap().render(&ctx);
filmstrip::show_raw(&mut app, &ctx, 1).unwrap();
let d = app.camera_raw.as_ref().unwrap();
assert_eq!((d.raw_open.as_ref().unwrap().document, d.layer_name.as_str(), d.params.exposure), (ids[1], "two.dng", 0.0));
assert_eq!(app.raw_shelf[0].kept.as_ref().map(|(p, _)| p.exposure), Some(1.0), "the first keeps its settings");
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 30}}})).unwrap().unwrap();
filmstrip::show_raw(&mut app, &ctx, 0).unwrap();
assert_eq!(app.camera_raw.as_ref().unwrap().params.exposure, 1.0, "back to the first, as it was left");
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
assert_eq!(r["opened"].as_array().map(Vec::len), Some(3), "{r}");
assert!(app.raw_shelf.is_empty() && app.camera_raw.is_none());
assert_eq!(app.session.documents().len(), 3, "the never-shown third opens too");
assert!(app.session.documents().iter().all(|st| st.saved_revision == st.revision), "Open leaves them unmodified");
// Cancel closes every raw in the filmstrip.
app.open_bytes("four.dng", &dng).unwrap();
app.open_bytes("five.dng", &dng).unwrap();
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
assert!(app.raw_shelf.is_empty());
assert_eq!(app.session.documents().len(), 3);
}
/// A raw whose Open fails stays in the filmstrip with its settings, and the dialog comes back
/// for it; the raws that opened leave it.
#[test]
fn a_raw_that_fails_to_open_stays_in_the_filmstrip_with_its_settings() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("one.dng", &dng).unwrap();
app.open_bytes("two.dng", &dng).unwrap();
app.open_bytes("three.dng", &dng).unwrap();
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
// Two's file can no longer be developed, and it was left with its own settings.
let mut kept = CameraRaw { exposure: 0.5, ..Default::default() };
kept.contrast = 20.0;
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file".to_vec()));
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
assert_eq!(r["opened"].as_array().map(Vec::len), Some(2), "one and three open: {r}");
let left: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
assert_eq!(left, vec![ids[1]], "only the failed raw stays in the filmstrip");
assert_eq!(app.raw_shelf[0].kept.as_ref().map(|(p, _)| p), Some(&kept), "with its settings");
// The dialog comes back for it with them; when every raw fails, the dialog and its
// filmstrip stay.
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
let shown = &app.camera_raw.as_ref().unwrap().params;
assert_eq!((shown.exposure, shown.contrast), (0.5, 20.0));
app.open_bytes("four.dng", &dng).unwrap();
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file either".to_vec()));
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().is_err());
assert_eq!(app.raw_shelf.len(), 2, "both raws stay in the filmstrip");
assert!(app.camera_raw.is_some(), "and the dialog stays open");
// Cancel still closes every raw in it.
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
assert!(app.raw_shelf.is_empty());
assert_eq!(app.session.documents().len(), 2, "one and three stay open");
}
/// A raw that can't be shown leaves things as they were: the dialog stays on the raw being left,
/// and the other keeps its place in the filmstrip with its settings.
#[test]
fn a_raw_that_cannot_be_shown_keeps_the_dialog_and_the_filmstrip() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("one.dng", &dng).unwrap();
app.open_bytes("two.dng", &dng).unwrap();
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
let kept = CameraRaw { exposure: 0.5, ..Default::default() };
app.raw_shelf[1].kept = Some((kept.clone(), CameraRaw::default()));
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
let two = document_index(&app, ids[1]).unwrap();
app.session.close(two);
assert!(filmstrip::show_raw(&mut app, &ctx, 1).is_err());
let d = app.camera_raw.as_ref().expect("the dialog stays");
assert_eq!((d.raw_open.as_ref().unwrap().document, d.params.exposure), (ids[0], 1.0));
assert_eq!(app.raw_shelf.len(), 2);
assert_eq!(app.raw_shelf[1].kept.as_ref().map(|(p, _)| p), Some(&kept));
}
/// Leaving a raw before its preview has caught up: its thumbnail shows the settings it keeps, not
/// the frame before them.
#[test]
fn a_raw_left_before_its_preview_lands_gets_a_thumbnail_of_its_settings() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("one.dng", &dng).unwrap();
app.open_bytes("two.dng", &dng).unwrap();
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
let d = app.camera_raw.as_mut().unwrap();
d.params.exposure = 2.0;
d.dirty = true;
filmstrip::keep_settings(&mut app);
let thumb = app.raw_shelf[0].thumb.clone().expect("a thumbnail");
app.camera_raw.as_mut().unwrap().render(&ctx);
filmstrip::keep_settings(&mut app);
assert!(thumb == app.raw_shelf[0].thumb.clone().unwrap(), "the thumbnail matches a finished develop of the same settings");
}
/// Open returns once a background develop has started; if that develop then fails, the raw comes
/// back to the filmstrip with its settings, as one that failed at once stays there.
#[test]
fn a_raw_whose_background_develop_fails_comes_back_to_the_filmstrip() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("one.dng", &dng).unwrap();
app.open_bytes("two.dng", &dng).unwrap();
let ids: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
let (kept, from) = (CameraRaw { exposure: 0.5, contrast: 20.0, ..Default::default() }, CameraRaw { contrast: 5.0, ..Default::default() });
app.raw_shelf[1].raw.bytes = Some(std::sync::Arc::new(b"not a raw file".to_vec()));
app.raw_shelf[1].kept = Some((kept.clone(), from.clone()));
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 1.0}}})).unwrap().unwrap();
app.background_jobs = true;
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
assert_eq!(r["opened"].as_array().map(Vec::len), Some(2), "both started: {r}");
assert!(app.raw_shelf.is_empty());
for _ in 0..3000 {
crate::jobs_ui::tick(&mut app, &ctx);
if app.raw_redevelop.is_empty() && !app.session.has_jobs() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(app.raw_redevelop.is_empty() && !app.session.has_jobs(), "the jobs never finished");
let left: Vec<_> = app.raw_shelf.iter().map(|h| h.raw.document).collect();
assert_eq!(left, vec![ids[1]], "only the failed raw comes back");
assert_eq!(app.raw_shelf[0].kept, Some((kept, from)), "with its settings");
app.background_jobs = false;
filmstrip::open_held(&mut app, &ctx, 0).unwrap();
assert_eq!(app.camera_raw.as_ref().unwrap().params.contrast, 20.0);
}
/// Camera Raw Defaults' Auto reads the same pixels whether a raw was shown or not, so Open gives
/// it the same settings either way: a fine pattern that a sparse sample misreads shows it.
#[test]
fn auto_tone_of_a_never_shown_raw_matches_the_dialogs() {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default());
let ctx = egui::Context::default();
app.run("prefs.set", json!({"path": "rawDefaults.applyAutoTone", "value": true})).unwrap();
app.run("file.new", json!({"width": 1800, "height": 1200})).unwrap();
app.run("layer.new.layer", json!({})).unwrap();
let st = app.session.active_mut().unwrap();
let id = st.doc.layers.iter().find(|l| l.surface().is_some()).unwrap().id;
st.active_layer = Some(id);
let document = st.doc.id;
let Some(photocraft_doc::LayerContent::Raster(s)) = std::sync::Arc::make_mut(&mut st.doc).layer_mut(id).map(|l| &mut l.content) else {
panic!("a pixel layer")
};
let format = s.format();
let (dark, light) = (photocraft_raster::from_rgba(&format, [0.1, 0.1, 0.1, 1.0]), photocraft_raster::from_rgba(&format, [0.9, 0.9, 0.9, 1.0]));
for y in 0..1200 {
for x in 0..1800 {
s.write_pixel(x, y, if (x + y) % 2 == 0 { &dark } else { &light });
}
}
let method = photocraft_io::raw::Demosaic::Ahd;
let raw = RawOpen { document, name: "pattern.dng".into(), path: None, bytes: None, developed_with: method, demosaic: method };
let (unshown, _) = filmstrip::first_settings(&app, &raw);
open_raw(&mut app, &ctx, raw).unwrap();
let shown = &app.camera_raw.as_ref().unwrap().params;
let tone = |p: &CameraRaw| [p.exposure, p.contrast, p.highlights, p.shadows, p.whites, p.blacks];
assert_eq!(tone(&unshown), tone(shown));
}
#[test]
fn cancel_closes_the_raw_and_the_preference_skips_the_dialog() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("a.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"cancel": true}})).unwrap().unwrap();
assert_eq!(r["cancelled"], true);
assert!(app.session.documents().is_empty(), "Cancel closes the raw being opened");
// Unchanged settings: Open keeps the default develop and adds no step.
app.open_bytes("b.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
assert_eq!((r["document"].as_u64(), r["redeveloped"].as_bool(), r.get("filter")), (Some(0), Some(false), None), "{r}");
@@ -546,7 +733,7 @@ fn cancel_closes_the_raw_and_the_preference_skips_the_dialog() {
// Preference off: raws open straight away.
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
app.open_bytes("c.dng", &dng).unwrap();
assert!(app.pending_raw_open.is_none());
assert!(app.raw_shelf.is_empty());
// A non-raw never queues the dialog; a closed document is an error, not a panic.
assert!(
open_raw(
@@ -570,7 +757,7 @@ fn calibration_changes_the_preview_and_round_trips_through_xmp() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("IMG_0005.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let before = app.camera_raw.as_ref().unwrap().processed.clone();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"shadowTint": 100, "calibrationHue": [100, 0, 0], "calibrationSat": [0, 0, -100]}}}))
@@ -595,7 +782,7 @@ fn auto_sets_the_light_sliders_from_the_image() {
let (mut app, dng) = raw_app();
let ctx = egui::Context::default();
app.open_bytes("IMG_0006.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
assert!(app.camera_raw.as_ref().unwrap().params.is_identity());
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"auto": true}})).unwrap().unwrap();
@@ -605,7 +792,7 @@ fn auto_sets_the_light_sliders_from_the_image() {
// The preference does the same when a raw opens without a sidecar.
app.run("prefs.set", json!({"path": "rawDefaults.applyAutoTone", "value": true})).unwrap();
app.open_bytes("IMG_0007.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
assert_eq!(app.camera_raw.as_ref().unwrap().params, p, "Apply auto tone opens the dialog with Auto's settings");
}
@@ -635,7 +822,7 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
}));
let ctx = egui::Context::default();
app.open_bytes("IMG_0008.cr2", &dng).unwrap();
let mut raw = app.pending_raw_open.take().unwrap();
let mut raw = take_queued(&mut app).unwrap();
raw.path = Some(raw_path.clone());
open_raw(&mut app, &ctx, raw).unwrap();
let d = app.camera_raw.as_ref().unwrap();
@@ -657,14 +844,14 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
let dng_path = dir.join("IMG_0009.dng").to_string_lossy().into_owned();
std::fs::write(&dng_path, &dng).unwrap();
app.open_bytes("IMG_0009.dng", &dng).unwrap();
let mut raw = app.pending_raw_open.take().unwrap();
let mut raw = take_queued(&mut app).unwrap();
raw.path = Some(dng_path);
open_raw(&mut app, &ctx, raw).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 15}, "commit": true}})).unwrap().unwrap();
assert!(written.lock().unwrap().is_empty(), "no sidecar for a DNG");
app.run("prefs.set", json!({"path": "rawDefaults.writeXmpSidecars", "value": false})).unwrap();
app.open_bytes("IMG_0008.cr2", &dng).unwrap();
let mut raw = app.pending_raw_open.take().unwrap();
let mut raw = take_queued(&mut app).unwrap();
raw.path = Some(raw_path.clone());
open_raw(&mut app, &ctx, raw).unwrap();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 30}, "commit": true}})).unwrap().unwrap();
@@ -673,7 +860,7 @@ fn a_sidecar_fills_the_dialog_and_open_writes_it_back() {
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
let before = app.session.documents().len();
app.open_file(&raw_path, &dng).unwrap();
assert!(app.pending_raw_open.is_none());
assert!(app.raw_shelf.is_empty());
assert_eq!(app.session.documents().len(), before + 1);
let st = app.session.active().unwrap();
assert_eq!(st.saved_revision, st.revision, "opening with a sidecar leaves the document unmodified");
@@ -686,7 +873,7 @@ fn camera_raw_defaults_apply_when_a_raw_opens() {
let (mut app, dng) = raw_app();
app.run("prefs.set", json!({"values": {"rawDefaults.openInCameraRaw": false, "rawDefaults.colorSpace": "srgb", "rawDefaults.bitDepth": "8", "rawDefaults.resolution": 240.0, "rawDefaults.sharpenFor": "screen", "rawDefaults.openAsSmartObject": true}})).unwrap();
app.open_bytes("IMG_0010.dng", &dng).unwrap();
assert!(app.pending_raw_open.is_none());
assert!(app.raw_shelf.is_empty());
let st = app.session.active().unwrap();
assert_eq!(st.doc.depth, photocraft_color::SampleType::U8, "Bit depth 8");
assert_eq!(st.doc.resolution_dpi, 240.0, "Resolution");
@@ -700,7 +887,7 @@ fn camera_raw_defaults_apply_when_a_raw_opens() {
.unwrap();
let ctx = egui::Context::default();
app.open_bytes("IMG_0011.dng", &dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(&mut app).unwrap();
open_raw(&mut app, &ctx, raw).unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"commit": true}})).unwrap().unwrap();
assert_eq!(r["defaults"], json!(["sharpenFor", "colorSpace"]), "{r}");
@@ -756,7 +943,7 @@ fn raw_with_sidecar(tag: &str, sidecar: Option<&str>) -> (PhotocraftApp, Vec<u8>
/// Opens the raw at `raw_path` in the open-time dialog, as File › Open does.
fn open_dialog_at(app: &mut PhotocraftApp, ctx: &egui::Context, dng: &[u8], raw_path: &str) {
app.open_bytes("IMG_0100.cr2", dng).unwrap();
let mut raw = app.pending_raw_open.take().unwrap();
let mut raw = take_queued(app).unwrap();
raw.path = Some(raw_path.to_string());
open_raw(app, ctx, raw).unwrap();
}
@@ -808,7 +995,7 @@ fn opening_without_the_dialog_does_not_rewrite_the_sidecar() {
let (mut app, dng, dir, raw_path, written) = raw_with_sidecar("nodialog", Some(LIGHTROOM_SIDECAR));
app.run("prefs.set", json!({"path": "rawDefaults.openInCameraRaw", "value": false})).unwrap();
app.open_file(&raw_path, &dng).unwrap();
assert!(app.pending_raw_open.is_none());
assert!(app.raw_shelf.is_empty());
assert_eq!(app.session.documents().len(), 1);
assert!(written.lock().unwrap().is_empty(), "just opening never writes the sidecar");
assert_eq!(std::fs::read_to_string(sidecar_path(&raw_path)).unwrap(), LIGHTROOM_SIDECAR);
@@ -855,7 +1042,7 @@ fn a_sidecar_write_error_stays_reported() {
fn open_with_settings(app: &mut PhotocraftApp, ctx: &egui::Context, dng: &[u8], name: &str) {
app.open_bytes(name, dng).unwrap();
let raw = app.pending_raw_open.take().unwrap();
let raw = take_queued(app).unwrap();
open_raw(app, ctx, raw).unwrap();
menu(app, ctx, "filter.cameraRaw", &json!({"ui": {"set": {"exposure": 0.5, "contrast": 60, "saturation": 40, "vibrance": 30}}})).unwrap().unwrap();
}
@@ -1022,3 +1209,60 @@ fn save_and_load_settings_round_trip_through_xmp_files() {
frame(&mut app);
assert_eq!(app.camera_raw.as_ref().unwrap().params.clarity, 25.0);
}
/// Done keeps the settings in the raw's sidecar and opens nothing,
/// even with the preference that writes sidecars on Open turned off.
#[test]
fn done_saves_the_sidecar_and_opens_nothing() {
let (mut app, dng, dir, raw_path, written) = raw_with_sidecar("done", None);
let ctx = egui::Context::default();
app.run("prefs.set", json!({"path": "rawDefaults.writeXmpSidecars", "value": false})).unwrap();
let before = app.session.documents().len();
open_dialog_at(&mut app, &ctx, &dng, &raw_path);
let set = json!({"contrast": 20, "clarity": 15});
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": set}})).unwrap().unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
assert_eq!(r["saved"], json!(["IMG_0100.xmp"]), "{r}");
assert!(app.camera_raw.is_none() && app.raw_shelf.is_empty());
assert_eq!(app.session.documents().len(), before, "Done opens nothing");
let w = written.lock().unwrap().clone();
assert_eq!(w.len(), 1);
let mut back = CameraRaw::default();
photocraft_io::camera_raw_xmp::load(&mut back, &w[0].1).unwrap();
assert_eq!((back.contrast, back.clarity), (20.0, 15.0));
// Done is only for raws being opened, and checked before anything else in the request
// changes the dialog.
app.run("file.new", json!({"width": 8, "height": 8})).unwrap();
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().is_err());
menu(&mut app, &ctx, "filter.cameraRaw", &json!({})).unwrap().unwrap();
let before = app.camera_raw.as_ref().unwrap().params.clone();
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 40}, "done": true}})).unwrap().is_err());
assert_eq!(app.camera_raw.as_ref().unwrap().params, before, "a refused Done changes nothing");
for other in ["commit", "cancel"] {
assert!(menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true, other: true}})).unwrap().is_err(), "{other}");
}
let _ = std::fs::remove_dir_all(&dir);
}
/// A sidecar that can't be written keeps the dialog and the raw open with their settings;
/// default settings are nothing to save, not an error.
#[test]
fn done_keeps_the_raws_open_when_a_sidecar_write_fails() {
let (mut app, dng, dir, raw_path, _) = raw_with_sidecar("done-fails", None);
let ctx = egui::Context::default();
app.services.write = Some(Box::new(|_: &str, _: &[u8]| Err("read-only volume".to_string())));
open_dialog_at(&mut app, &ctx, &dng, &raw_path);
let docs = app.session.documents().len();
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 20}}})).unwrap().unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
assert_eq!(r["done"], false, "{r}");
assert_eq!(app.camera_raw.as_ref().map(|d| d.params.contrast), Some(20.0), "the edit is still there");
assert_eq!(app.session.documents().len(), docs, "the raw stays open");
assert!(app.ui.status_error);
// At the defaults there is nothing to write: Done closes, without an error.
menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"set": {"contrast": 0}}})).unwrap().unwrap();
let r = menu(&mut app, &ctx, "filter.cameraRaw", &json!({"ui": {"done": true}})).unwrap().unwrap();
assert_eq!((r["done"].clone(), r["saved"].clone()), (json!(true), json!([])), "{r}");
assert!(app.camera_raw.is_none() && !app.ui.status_error, "{}", app.ui.status);
let _ = std::fs::remove_dir_all(&dir);
}
+5
View File
@@ -4356,6 +4356,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Zobrazit hranice vrstvy při najetí
Expand layer group on click Rozbalit skupinu vrstev kliknutím
Show hover bounds from Layers Zobrazit hranice při najetí z panelu Vrstvy
Saved Camera Raw settings to {file} Nastavení Camera Raw uložena do {file}
Save the settings to each raw's sidecar without opening it Uložit nastavení do souboru sidecar každého snímku raw bez jeho otevření
No settings saved: these raws can't keep a sidecar here Nic neuloženo: tyto snímky raw zde nemohou mít soubor sidecar
Some settings could not be saved; the raws stay open Některá nastavení nelze uložit; snímky raw zůstávají otevřené
Nothing to save: these raws are at their default settings Není co ukládat: tyto snímky raw mají výchozí nastavení
# Artboard tool options bar (#11385)
Distribute Widths Rozmístit šířky
Distribute Heights Rozmístit výšky
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3651,6 +3651,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Ebenenbegrenzung beim Darüberfahren anzeigen
Expand layer group on click Ebenengruppe per Klick erweitern
Show hover bounds from Layers Begrenzung beim Darüberfahren aus Ebenen anzeigen
Saved Camera Raw settings to {file} Camera-Raw-Einstellungen in {file} gespeichert
Save the settings to each raw's sidecar without opening it Die Einstellungen in der Filialdatei jeder Raw-Datei speichern, ohne sie zu öffnen
No settings saved: these raws can't keep a sidecar here Nichts gespeichert: Diese Raw-Dateien können hier keine Filialdatei haben
Some settings could not be saved; the raws stay open Einige Einstellungen konnten nicht gespeichert werden; die Raw-Dateien bleiben geöffnet
Nothing to save: these raws are at their default settings Nichts zu speichern: Diese Raw-Dateien haben ihre Standardeinstellungen
# Artboard tool options bar (#11385)
Distribute Widths Breiten verteilen
Distribute Heights Höhen verteilen
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4315,6 +4315,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Εμφάνιση ορίων στρώσης στο πέρασμα του δείκτη
Expand layer group on click Ανάπτυξη ομάδας στρώσεων με κλικ
Show hover bounds from Layers Εμφάνιση ορίων στο πέρασμα από τις Στρώσεις
Saved Camera Raw settings to {file} Οι ρυθμίσεις Camera Raw αποθηκεύτηκαν στο {file}
Save the settings to each raw's sidecar without opening it Αποθήκευση των ρυθμίσεων στο συνοδευτικό αρχείο κάθε raw χωρίς άνοιγμα
No settings saved: these raws can't keep a sidecar here Δεν αποθηκεύτηκε τίποτα: αυτά τα raw δεν μπορούν να έχουν συνοδευτικό αρχείο εδώ
Some settings could not be saved; the raws stay open Ορισμένες ρυθμίσεις δεν ήταν δυνατό να αποθηκευτούν· τα raw παραμένουν ανοιχτά
Nothing to save: these raws are at their default settings Τίποτα για αποθήκευση: αυτά τα raw έχουν τις προεπιλεγμένες ρυθμίσεις
# Artboard tool options bar (#11385)
Distribute Widths Κατανομή πλατών
Distribute Heights Κατανομή υψών
Can't render this file because it contains an unexpected character in line 47 and column 53.
+5
View File
@@ -4515,6 +4515,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Mostrar límites de capa al pasar el puntero
Expand layer group on click Expandir grupo de capas al hacer clic
Show hover bounds from Layers Mostrar límites al pasar el puntero desde Capas
Saved Camera Raw settings to {file} Ajustes de Camera Raw guardados en {file}
Save the settings to each raw's sidecar without opening it Guardar los ajustes en el archivo sidecar de cada raw sin abrirlo
No settings saved: these raws can't keep a sidecar here No se guardó nada: estos raw no pueden tener un archivo sidecar aquí
Some settings could not be saved; the raws stay open No se pudieron guardar algunos ajustes; los raw siguen abiertos
Nothing to save: these raws are at their default settings Nada que guardar: estos raw tienen los ajustes predeterminados
# Artboard tool options bar (#11385)
Distribute Widths Distribuir anchos
Distribute Heights Distribuir alturas
Can't render this file because it contains an unexpected character in line 4 and column 47.
+5
View File
@@ -4638,6 +4638,11 @@ shape Appearance Apparence
Show layer bounds on hover Afficher les limites du calque au survol
Expand layer group on click Développer le groupe de calques au clic
Show hover bounds from Layers Afficher les limites au survol depuis Calques
Saved Camera Raw settings to {file} Réglages Camera Raw enregistrés dans {file}
Save the settings to each raw's sidecar without opening it Enregistrer les réglages dans le fichier annexe de chaque raw sans l’ouvrir
No settings saved: these raws can't keep a sidecar here Rien n’a été enregistré : ces raw ne peuvent pas avoir de fichier annexe ici
Some settings could not be saved; the raws stay open Certains réglages n’ont pas pu être enregistrés ; les raw restent ouverts
Nothing to save: these raws are at their default settings Rien à enregistrer : ces raw ont leurs réglages par défaut
# Artboard tool options bar (#11385)
Distribute Widths Répartir les largeurs
Distribute Heights Répartir les hauteurs
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4522,6 +4522,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Réteghatárok megjelenítése rámutatáskor
Expand layer group on click Rétegcsoport kibontása kattintásra
Show hover bounds from Layers Határok megjelenítése a Rétegek panelről rámutatáskor
Saved Camera Raw settings to {file} Camera Raw-beállítások mentve ide: {file}
Save the settings to each raw's sidecar without opening it A beállítások mentése minden raw kísérőfájljába megnyitás nélkül
No settings saved: these raws can't keep a sidecar here Nincs mentve semmi: ezek a raw fájlok itt nem kaphatnak kísérőfájlt
Some settings could not be saved; the raws stay open Néhány beállítást nem sikerült menteni; a raw fájlok nyitva maradnak
Nothing to save: these raws are at their default settings Nincs mit menteni: ezek a raw fájlok alapértelmezett beállításokon vannak
# Artboard tool options bar (#11385)
Distribute Widths Szélességek elosztása
Distribute Heights Magasságok elosztása
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3638,6 +3638,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Tampilkan batas layer saat kursor di atasnya
Expand layer group on click Perluas grup layer saat diklik
Show hover bounds from Layers Tampilkan batas saat kursor di atas dari Layer
Saved Camera Raw settings to {file} Pengaturan Camera Raw disimpan ke {file}
Save the settings to each raw's sidecar without opening it Simpan pengaturan ke file sidecar setiap raw tanpa membukanya
No settings saved: these raws can't keep a sidecar here Tidak ada yang disimpan: raw ini tidak dapat memiliki file sidecar di sini
Some settings could not be saved; the raws stay open Beberapa pengaturan tidak dapat disimpan; raw tetap terbuka
Nothing to save: these raws are at their default settings Tidak ada yang perlu disimpan: raw ini memakai pengaturan default
# Artboard tool options bar (#11385)
Distribute Widths Distribusi lebar
Distribute Heights Distribusi tinggi
Can't render this file because it contains an unexpected character in line 4 and column 47.
+5
View File
@@ -4714,6 +4714,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Mostra i limiti del livello al passaggio del mouse
Expand layer group on click Espandi il gruppo di livelli al clic
Show hover bounds from Layers Mostra i limiti al passaggio dal pannello Livelli
Saved Camera Raw settings to {file} Impostazioni Camera Raw salvate in {file}
Save the settings to each raw's sidecar without opening it Salva le impostazioni nel file sidecar di ogni raw senza aprirlo
No settings saved: these raws can't keep a sidecar here Nulla salvato: questi raw non possono avere un file sidecar qui
Some settings could not be saved; the raws stay open Alcune impostazioni non sono state salvate; i raw restano aperti
Nothing to save: these raws are at their default settings Niente da salvare: questi raw hanno le impostazioni predefinite
# Artboard tool options bar (#11385)
Distribute Widths Distribuisci larghezze
Distribute Heights Distribuisci altezze
Can't render this file because it contains an unexpected character in line 79 and column 53.
+5
View File
@@ -4286,6 +4286,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover ホバー時にレイヤーの境界を表示
Expand layer group on click クリックでレイヤーグループを展開
Show hover bounds from Layers レイヤーパネルからのホバー境界を表示
Saved Camera Raw settings to {file} Camera Raw の設定を {file} に保存しました
Save the settings to each raw's sidecar without opening it 各 Raw のサイドカーファイルに設定を保存し、開かずに閉じます
No settings saved: these raws can't keep a sidecar here 保存されませんでした:これらの Raw にはここでサイドカーファイルを作成できません
Some settings could not be saved; the raws stay open 一部の設定を保存できませんでした。Raw は開いたままです
Nothing to save: these raws are at their default settings 保存するものはありません:これらの Raw は初期設定のままです
# Artboard tool options bar (#11385)
Distribute Widths 幅を分布
Distribute Heights 高さを分布
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4188,6 +4188,11 @@ shape Appearance 화면 모양
Show layer bounds on hover 마우스를 올리면 레이어 경계 표시
Expand layer group on click 클릭하면 레이어 그룹 펼치기
Show hover bounds from Layers 레이어 패널에서 마우스를 올릴 때 경계 표시
Saved Camera Raw settings to {file} Camera Raw 설정을 {file}에 저장했습니다
Save the settings to each raw's sidecar without opening it 각 Raw의 사이드카 파일에 설정을 저장하고 열지 않습니다
No settings saved: these raws can't keep a sidecar here 저장된 항목 없음: 이 Raw 파일은 여기에 사이드카 파일을 둘 수 없습니다
Some settings could not be saved; the raws stay open 일부 설정을 저장할 수 없습니다. Raw 파일은 열린 상태로 유지됩니다
Nothing to save: these raws are at their default settings 저장할 항목 없음: 이 Raw 파일은 기본 설정입니다
# Artboard tool options bar (#11385)
Distribute Widths 너비 분포
Distribute Heights 높이 분포
Can't render this file because it contains an unexpected character in line 63 and column 53.
+5
View File
@@ -4369,6 +4369,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Laaggrenzen tonen bij aanwijzen
Expand layer group on click Laaggroep uitvouwen bij klikken
Show hover bounds from Layers Aanwijsgrenzen tonen vanuit Lagen
Saved Camera Raw settings to {file} Camera Raw-instellingen opgeslagen in {file}
Save the settings to each raw's sidecar without opening it De instellingen in het sidecarbestand van elke raw opslaan zonder die te openen
No settings saved: these raws can't keep a sidecar here Niets opgeslagen: deze raws kunnen hier geen sidecarbestand krijgen
Some settings could not be saved; the raws stay open Sommige instellingen konden niet worden opgeslagen; de raws blijven open
Nothing to save: these raws are at their default settings Niets op te slaan: deze raws staan op hun standaardinstellingen
# Artboard tool options bar (#11385)
Distribute Widths Breedtes verdelen
Distribute Heights Hoogtes verdelen
Can't render this file because it contains an unexpected character in line 8 and column 34.
+5
View File
@@ -4233,6 +4233,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Pokaż granice warstwy po najechaniu
Expand layer group on click Rozwiń grupę warstw po kliknięciu
Show hover bounds from Layers Pokaż granice po najechaniu z panelu Warstwy
Saved Camera Raw settings to {file} Zapisano ustawienia Camera Raw w {file}
Save the settings to each raw's sidecar without opening it Zapisz ustawienia w pliku sidecar każdego pliku raw bez otwierania go
No settings saved: these raws can't keep a sidecar here Nic nie zapisano: te pliki raw nie mogą tu mieć pliku sidecar
Some settings could not be saved; the raws stay open Nie udało się zapisać niektórych ustawień; pliki raw pozostają otwarte
Nothing to save: these raws are at their default settings Nic do zapisania: te pliki raw mają ustawienia domyślne
# Artboard tool options bar (#11385)
Distribute Widths Rozmieść szerokości równomiernie
Distribute Heights Rozmieść wysokości równomiernie
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4529,6 +4529,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Mostrar limites da camada ao passar o cursor
Expand layer group on click Expandir grupo de camadas ao clicar
Show hover bounds from Layers Mostrar limites ao passar o cursor em Camadas
Saved Camera Raw settings to {file} Configurações do Camera Raw salvas em {file}
Save the settings to each raw's sidecar without opening it Salvar as configurações no arquivo sidecar de cada raw sem abri-lo
No settings saved: these raws can't keep a sidecar here Nada foi salvo: estes raw não podem ter um arquivo sidecar aqui
Some settings could not be saved; the raws stay open Não foi possível salvar algumas configurações; os raw continuam abertos
Nothing to save: these raws are at their default settings Nada para salvar: estes raw estão nas configurações padrão
# Artboard tool options bar (#11385)
Distribute Widths Distribuir larguras
Distribute Heights Distribuir alturas
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4800,6 +4800,11 @@ shape Appearance Оформление
Show layer bounds on hover Показывать границы слоя при наведении
Expand layer group on click Раскрывать группу слоёв по щелчку
Show hover bounds from Layers Показывать границы при наведении из панели «Слои»
Saved Camera Raw settings to {file} Настройки Camera Raw сохранены в {file}
Save the settings to each raw's sidecar without opening it Сохранить настройки в сопроводительный файл каждого raw, не открывая его
No settings saved: these raws can't keep a sidecar here Ничего не сохранено: у этих raw здесь не может быть сопроводительного файла
Some settings could not be saved; the raws stay open Не удалось сохранить некоторые настройки; raw остаются открытыми
Nothing to save: these raws are at their default settings Нечего сохранять: у этих raw настройки по умолчанию
# Artboard tool options bar (#11385)
Distribute Widths Распределить ширину
Distribute Heights Распределить высоту
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4681,6 +4681,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Prikaži granice sloja pri prelasku miša
Expand layer group on click Proširi grupu slojeva klikom
Show hover bounds from Layers Prikaži granice pri prelasku iz panela Slojevi
Saved Camera Raw settings to {file} Подешавања Camera Raw сачувана у {file}
Save the settings to each raw's sidecar without opening it Sačuvaj podešavanja u prateću datoteku svakog raw snimka bez otvaranja
No settings saved: these raws can't keep a sidecar here Ništa nije sačuvano: ovi raw snimci ovde ne mogu imati prateću datoteku
Some settings could not be saved; the raws stay open Neka podešavanja nije moguće sačuvati; raw snimci ostaju otvoreni
Nothing to save: these raws are at their default settings Nema šta da se sačuva: ovi raw snimci imaju podrazumevana podešavanja
# Artboard tool options bar (#11385)
Distribute Widths Raspodeli širine
Distribute Heights Raspodeli visine
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -4346,6 +4346,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Üzerine gelince katman sınırlarını göster
Expand layer group on click Tıklayınca katman grubunu genişlet
Show hover bounds from Layers Katmanlar'dan üzerine gelme sınırlarını göster
Saved Camera Raw settings to {file} Camera Raw ayarları {file} dosyasına kaydedildi
Save the settings to each raw's sidecar without opening it Ayarları her raw dosyasının yan dosyasına kaydet, açmadan
No settings saved: these raws can't keep a sidecar here Hiçbir şey kaydedilmedi: bu raw dosyaları burada yan dosyaya sahip olamaz
Some settings could not be saved; the raws stay open Bazı ayarlar kaydedilemedi; raw dosyaları açık kalıyor
Nothing to save: these raws are at their default settings Kaydedilecek bir şey yok: bu raw dosyaları varsayılan ayarlarda
# Artboard tool options bar (#11385)
Distribute Widths Genişlikleri Dağıt
Distribute Heights Yükseklikleri Dağıt
Can't render this file because it contains an unexpected character in line 8 and column 34.
+5
View File
@@ -4326,6 +4326,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Показувати межі шару під час наведення
Expand layer group on click Розгортати групу шарів за клацанням
Show hover bounds from Layers Показувати межі під час наведення з панелі «Шари»
Saved Camera Raw settings to {file} Налаштування Camera Raw збережено в {file}
Save the settings to each raw's sidecar without opening it Зберегти налаштування в супровідний файл кожного raw, не відкриваючи його
No settings saved: these raws can't keep a sidecar here Нічого не збережено: ці raw тут не можуть мати супровідного файлу
Some settings could not be saved; the raws stay open Не вдалося зберегти деякі налаштування; raw залишаються відкритими
Nothing to save: these raws are at their default settings Нічого зберігати: ці raw мають типові налаштування
# Artboard tool options bar (#11385)
Distribute Widths Розподілити ширину
Distribute Heights Розподілити висоту
Can't render this file because it contains an unexpected character in line 152 and column 53.
+5
View File
@@ -4120,6 +4120,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover Hiện ranh giới lớp khi di chuột
Expand layer group on click Mở rộng nhóm lớp khi nhấp
Show hover bounds from Layers Hiện ranh giới khi di chuột từ bảng Lớp
Saved Camera Raw settings to {file} Đã lưu cài đặt Camera Raw vào {file}
Save the settings to each raw's sidecar without opening it Lưu thiết lập vào tệp sidecar của từng ảnh raw mà không mở
No settings saved: these raws can't keep a sidecar here Không lưu được gì: các ảnh raw này không thể có tệp sidecar ở đây
Some settings could not be saved; the raws stay open Không thể lưu một số cài đặt; các ảnh raw vẫn mở
Nothing to save: these raws are at their default settings Không có gì để lưu: các ảnh raw này đang ở cài đặt mặc định
# Artboard tool options bar (#11385)
Distribute Widths Phân bố chiều rộng
Distribute Heights Phân bố chiều cao
Can't render this file because it contains an unexpected character in line 6 and column 47.
+5
View File
@@ -3307,6 +3307,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover 悬停时显示图层边界
Expand layer group on click 单击时展开图层组
Show hover bounds from Layers 显示来自图层面板的悬停边界
Saved Camera Raw settings to {file} 已将 Camera Raw 设置保存到 {file}
Save the settings to each raw's sidecar without opening it 将设置保存到每个 Raw 的附属文件,不打开它
No settings saved: these raws can't keep a sidecar here 未保存任何内容:这些 Raw 在此处无法拥有附属文件
Some settings could not be saved; the raws stay open 部分设置无法保存;Raw 保持打开
Nothing to save: these raws are at their default settings 无需保存:这些 Raw 均为默认设置
# Artboard tool options bar (#11385)
Distribute Widths 分布宽度
Distribute Heights 分布高度
Can't render this file because it contains an unexpected character in line 63 and column 53.
+5
View File
@@ -3264,6 +3264,11 @@ smartSvg AI/PDF/EPS contents will be edited as SVG. Saving replaces the embedded
Show layer bounds on hover 滑過時顯示圖層邊界
Expand layer group on click 按一下時展開圖層群組
Show hover bounds from Layers 顯示來自圖層面板的滑過邊界
Saved Camera Raw settings to {file} 已將 Camera Raw 設定儲存到 {file}
Save the settings to each raw's sidecar without opening it 將設定儲存到每個 Raw 的附屬檔案,不開啟它
No settings saved: these raws can't keep a sidecar here 未儲存任何內容:這些 Raw 在此處無法擁有附屬檔案
Some settings could not be saved; the raws stay open 部分設定無法儲存;Raw 保持開啟
Nothing to save: these raws are at their default settings 無需儲存:這些 Raw 均為預設設定
# Artboard tool options bar (#11385)
Distribute Widths 均分寬度
Distribute Heights 均分高度
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -606,9 +606,9 @@ pub struct PhotocraftApp {
pub(crate) gradient: gradient_ui::LiveGradient,
/// Filter › Camera Raw Filter dialog (camera_raw_ui).
pub(crate) camera_raw: Option<camera_raw_ui::CameraRawDialog>,
/// A raw file just opened interactively, waiting for the open-time Camera Raw dialog (shown
/// on the next frame, which has the egui context).
pub(crate) pending_raw_open: Option<camera_raw_ui::RawOpen>,
/// Raws just opened interactively, waiting in the open-time Camera Raw dialog (shown on the
/// next frame, which has the egui context); several share it in a filmstrip.
pub(crate) raw_shelf: Vec<camera_raw_ui::HeldRaw>,
/// The open-time re-develops (or their Camera Raw steps) running in the background, one per
/// raw being opened.
pub(crate) raw_redevelop: Vec<camera_raw_ui::Redevelop>,