Fix SVG selection and import detection (#2224)
* Fix SVG import detection and configurable group depth * Fix malformed SVG error routing and formatting * Refresh scorecard for SVG depth preference --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1b6150e3fc
commit
265f10b91d
|
|
@ -21,6 +21,7 @@ const OPEN_EXTS: &[&str] = &[
|
|||
"pam", "pfm", "heic", "heif", "hif", "dng", "cr2", "cr3", "nef", "nrw", "arw", "pef", "orf", "rw2", "raf", "abr", "grd", "svg", "svgz", "aco", "ase", "af",
|
||||
"afdesign", "afphoto", "afpub",
|
||||
];
|
||||
const SVG_EXTS: &[&str] = &["svg", "svgz"];
|
||||
const CANVAS_ID: &str = "photocraft_canvas";
|
||||
|
||||
/// Fonts the host serves next to the page (`photocraft_ui_egui::served_fonts`): craft-fonts' manifest
|
||||
|
|
@ -335,7 +336,9 @@ impl eframe::App for WebShell {
|
|||
fn services(inbox: Inbox) -> Services {
|
||||
Services {
|
||||
screen_pick: screen_color_service(),
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| photocraft_io::import(name, bytes).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string()))),
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8], max_svg_group_depth: usize| {
|
||||
photocraft_io::import_with_svg_group_depth(name, bytes, max_svg_group_depth).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
|
||||
})),
|
||||
export: Some(Box::new(|doc: &Document, path: &str, settings: &photocraft_ui_egui::ExportSettings| {
|
||||
let mut opts = photocraft_io::ExportOptions::default();
|
||||
if let Some(q) = settings.jpeg_quality {
|
||||
|
|
@ -355,7 +358,7 @@ fn services(inbox: Inbox) -> Services {
|
|||
let dialog = if let Some(exts) = extensions {
|
||||
rfd::AsyncFileDialog::new().add_filter("Supported Files", &exts)
|
||||
} else {
|
||||
rfd::AsyncFileDialog::new().add_filter("All Formats", OPEN_EXTS)
|
||||
rfd::AsyncFileDialog::new().add_filter("All Formats", OPEN_EXTS).add_filter("SVG", SVG_EXTS)
|
||||
};
|
||||
let picked = dialog.pick_file().await;
|
||||
let answer = match picked {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const OPEN_EXTS: &[&str] = &[
|
|||
"pam", "pfm", "heic", "heif", "hif", "dng", "cr2", "cr3", "nef", "nrw", "arw", "pef", "orf", "rw2", "raf", "abr", "grd", "svg", "svgz", "aco", "ase",
|
||||
"kys", "af", "afdesign", "afphoto", "afpub",
|
||||
];
|
||||
const SVG_EXTS: &[&str] = &["svg", "svgz"];
|
||||
|
||||
/// Open dialog extensions that also match uppercase and mixed-case names (`IMG_0001.JPG`).
|
||||
/// On Linux and the BSDs rfd turns each extension into a case-sensitive `*.ext` glob for the XDG
|
||||
|
|
@ -35,6 +36,12 @@ fn open_filter_extensions(extensions: &[&str]) -> Vec<String> {
|
|||
extensions.iter().map(|ext| if case_sensitive_globs { ext.chars().map(class).collect() } else { ext.to_string() }).collect()
|
||||
}
|
||||
|
||||
/// An explicit SVG filter keeps vector artwork discoverable even when a native picker defaults to
|
||||
/// the PhotoCraft-only filter. SVG is also included in All Formats for normal multi-format opens.
|
||||
fn open_filters() -> Vec<(&'static str, Vec<String>)> {
|
||||
vec![("All Formats", open_filter_extensions(OPEN_EXTS)), ("PhotoCraft", open_filter_extensions(&["pcraft"])), ("SVG", open_filter_extensions(SVG_EXTS))]
|
||||
}
|
||||
|
||||
/// File › Save As formats: (filter name, extensions). The filter matching the suggested name's
|
||||
/// extension comes first, so a .pcraft document saves as .pcraft by default and everything else
|
||||
/// keeps defaulting to PSD.
|
||||
|
|
@ -88,9 +95,9 @@ fn show_file_dialog(request: FileDialogRequest, parent: Option<&eframe::Frame>,
|
|||
let dialog = if let Some(exts) = extensions {
|
||||
dialog.add_filter("Supported Files", &open_filter_extensions(&exts.iter().map(String::as_str).collect::<Vec<_>>()))
|
||||
} else {
|
||||
dialog
|
||||
.add_filter("All Formats", &open_filter_extensions(OPEN_EXTS))
|
||||
.add_filter("PhotoCraft", &open_filter_extensions(&["pcraft"]))
|
||||
open_filters()
|
||||
.into_iter()
|
||||
.fold(dialog, |dialog, (name, extensions)| dialog.add_filter(name, &extensions))
|
||||
.add_filter("Paint.NET", &open_filter_extensions(&["pdn"]))
|
||||
};
|
||||
if multiple {
|
||||
|
|
@ -310,8 +317,10 @@ pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) ->
|
|||
}) as photocraft_ui_egui::AutomationCommandFn
|
||||
});
|
||||
Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| {
|
||||
crate::crash_guard::guard("Open", || photocraft_io::import(name, bytes).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string()))
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8], max_svg_group_depth: usize| {
|
||||
crate::crash_guard::guard("Open", || {
|
||||
photocraft_io::import_with_svg_group_depth(name, bytes, max_svg_group_depth).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
|
||||
})
|
||||
})),
|
||||
export: Some(Box::new(|doc: &Document, path: &str, settings: &photocraft_ui_egui::ExportSettings| {
|
||||
let opts = export_options(settings);
|
||||
|
|
@ -475,6 +484,9 @@ mod tests {
|
|||
assert!(!matches(glob, &format!("{ext}x")) && !matches(glob, &ext[..ext.len() - 1]), "{glob} matches only .{ext}");
|
||||
}
|
||||
assert!(!globs.iter().any(|glob| matches(glob, "txt")));
|
||||
let svg_filter = open_filters().into_iter().find(|(name, _)| *name == "SVG").map(|(_, exts)| exts).unwrap_or_default();
|
||||
assert_eq!(svg_filter, open_filter_extensions(SVG_EXTS));
|
||||
assert!(svg_filter.iter().any(|glob| matches(glob, "svg")) && svg_filter.iter().any(|glob| matches(glob, "svgz")));
|
||||
}
|
||||
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
|
|
@ -482,6 +494,7 @@ mod tests {
|
|||
fn open_filters_keep_literal_extensions_on_windows_and_macos() {
|
||||
assert_eq!(open_filter_extensions(OPEN_EXTS), OPEN_EXTS);
|
||||
assert_eq!(open_filter_extensions(&["pcraft"]), ["pcraft"]);
|
||||
assert_eq!(open_filters().into_iter().find(|(name, _)| *name == "SVG").map(|(_, exts)| exts), Some(vec!["svg".to_string(), "svgz".to_string()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -397,7 +397,14 @@ pub fn place_bytes(s: &mut Session, name: &str, bytes: Vec<u8>, linked: Option<S
|
|||
let (cw, ch) = (d.doc.size.width as f64, d.doc.size.height as f64);
|
||||
let fmt = d.doc.pixel_format();
|
||||
let vector = photocraft_io::svg::is_svg(&bytes);
|
||||
let src = import(name, &bytes)?;
|
||||
let max_svg_group_depth = s.prefs().file_handling.rasterize_svg_groups_deeper_than as usize;
|
||||
let imported = photocraft_io::import_with_svg_group_depth(name, &bytes, max_svg_group_depth).map_err(|e| EngineError::Other(format!("{name}: {e}")))?;
|
||||
if imported.preview_only {
|
||||
return Err(EngineError::Other(format!(
|
||||
"{name}: only this Affinity file's embedded preview could be read; open it with File › Open to see the warning, or export PSD or PNG from Affinity before using it here"
|
||||
)));
|
||||
}
|
||||
let src = imported.document;
|
||||
let (w, h) = (src.size.width as f64, src.size.height as f64);
|
||||
let scale = match f64_param(p, "scale") {
|
||||
Some(k) => (k / 100.0).max(1e-4),
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ impl Session {
|
|||
let label = format!("Opening {name}");
|
||||
let name_w = name.to_string();
|
||||
let name_a = name.to_string();
|
||||
let max_svg_group_depth = self.prefs().file_handling.rasterize_svg_groups_deeper_than as usize;
|
||||
self.start_job(
|
||||
OPEN_JOB,
|
||||
json!({"name": name}),
|
||||
|
|
@ -430,7 +431,8 @@ impl Session {
|
|||
};
|
||||
ctx.check()?;
|
||||
ctx.progress(0.02, "Decoding");
|
||||
ctx.stage(0.02, 1.0, "Decoding", |ctl| photocraft_io::import_with(&name_w, &bytes, ctl)).map_err(|e| match e {
|
||||
ctx.stage(0.02, 1.0, "Decoding", |ctl| photocraft_io::import_with_svg_group_depth_and_interrupt(&name_w, &bytes, max_svg_group_depth, ctl))
|
||||
.map_err(|e| match e {
|
||||
photocraft_io::IoError::Cancelled => EngineError::Cancelled,
|
||||
e => EngineError::Other(e.to_string()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -365,6 +365,8 @@ pub struct FileHandling {
|
|||
pub ignore_exif_profile_tag: bool,
|
||||
pub ask_before_saving_layered_tiff: bool,
|
||||
pub maximize_psd_compatibility: Ask,
|
||||
/// SVG groups deeper than this are rasterised on import; parser and document safety caps remain fixed.
|
||||
pub rasterize_svg_groups_deeper_than: u32,
|
||||
pub recent_file_count: u32,
|
||||
/// Most recently opened files, newest first (File › Open Recent).
|
||||
pub recent_files: Vec<String>,
|
||||
|
|
@ -382,6 +384,7 @@ impl Default for FileHandling {
|
|||
ignore_exif_profile_tag: false,
|
||||
ask_before_saving_layered_tiff: true,
|
||||
maximize_psd_compatibility: Ask::Always,
|
||||
rasterize_svg_groups_deeper_than: photocraft_doc::MAX_GROUP_DEPTH as u32,
|
||||
recent_file_count: 20,
|
||||
recent_files: Vec::new(),
|
||||
}
|
||||
|
|
@ -950,6 +953,7 @@ pub fn range(path: &str) -> Option<(f64, f64)> {
|
|||
Some(match path {
|
||||
"fileHandling.autosaveMinutes" => (1.0, 240.0),
|
||||
"fileHandling.recentFileCount" => (0.0, 100.0),
|
||||
"fileHandling.rasterizeSvgGroupsDeeperThan" => (0.0, photocraft_doc::MAX_GROUP_DEPTH as f64),
|
||||
"interface.notificationDurationSeconds" => (1.0, 120.0),
|
||||
"export.jpegQuality" | "export.webpQuality" => (1.0, 100.0),
|
||||
"performance.memoryUsageMb" => (256.0, 1_048_576.0),
|
||||
|
|
|
|||
|
|
@ -361,6 +361,19 @@ fn choice_and_range_tables_cover_enum_fields() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_group_rasterization_threshold_is_bounded_by_document_depth() {
|
||||
let mut s = session();
|
||||
let pref = "fileHandling.rasterizeSvgGroupsDeeperThan";
|
||||
assert_eq!(s.execute("prefs.get", json!({"path": pref})).unwrap(), json!(photocraft_doc::MAX_GROUP_DEPTH));
|
||||
s.execute("prefs.set", json!({"path": pref, "value": 4})).unwrap();
|
||||
assert_eq!(s.prefs().file_handling.rasterize_svg_groups_deeper_than, 4);
|
||||
s.execute("prefs.set", json!({"path": pref, "value": 0})).unwrap();
|
||||
assert_eq!(s.prefs().file_handling.rasterize_svg_groups_deeper_than, 0, "zero disables the preference threshold");
|
||||
assert!(s.execute("prefs.set", json!({"path": pref, "value": photocraft_doc::MAX_GROUP_DEPTH + 1})).is_err());
|
||||
assert_eq!(s.prefs().file_handling.rasterize_svg_groups_deeper_than, 0, "out-of-range preferences leave the valid value unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_click_with_painting_tools_pref() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -171,20 +171,36 @@ pub fn is_psd(bytes: &[u8]) -> bool {
|
|||
/// Imports a file. PSD/PSB, Affinity containers and camera raws are detected by magic; everything
|
||||
/// else is decoded with `photocraft-codecs`.
|
||||
pub fn import(name: &str, bytes: &[u8]) -> Result<ImportResult, IoError> {
|
||||
import_with(name, bytes, &photocraft_raster::Interrupt::NONE)
|
||||
import_with_svg_group_depth(name, bytes, photocraft_doc::MAX_GROUP_DEPTH)
|
||||
}
|
||||
|
||||
/// [`import`] for a background open: checks `ctl` between stages (and per layer for PSD/PSB) and
|
||||
/// reports progress. A cancelled import fails with [`IoError::Cancelled`].
|
||||
pub fn import_with(name: &str, bytes: &[u8], ctl: &photocraft_raster::Interrupt) -> Result<ImportResult, IoError> {
|
||||
import_with_svg_group_depth_and_interrupt(name, bytes, photocraft_doc::MAX_GROUP_DEPTH, ctl)
|
||||
}
|
||||
|
||||
/// Imports a file with a configurable SVG editability threshold. SVG groups deeper than this are
|
||||
/// rasterised during conversion; the XML parser safety cap and document nesting limit stay fixed.
|
||||
pub fn import_with_svg_group_depth(name: &str, bytes: &[u8], max_group_depth: usize) -> Result<ImportResult, IoError> {
|
||||
import_with_svg_group_depth_and_interrupt(name, bytes, max_group_depth, &photocraft_raster::Interrupt::NONE)
|
||||
}
|
||||
|
||||
/// [`import_with`] with the SVG editable-group threshold selected by the user.
|
||||
pub fn import_with_svg_group_depth_and_interrupt(
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
max_group_depth: usize,
|
||||
ctl: &photocraft_raster::Interrupt,
|
||||
) -> Result<ImportResult, IoError> {
|
||||
ctl.check().map_err(|_| IoError::Cancelled)?;
|
||||
let r = import_stages(name, bytes, ctl)?;
|
||||
let r = import_stages(name, bytes, ctl, max_group_depth)?;
|
||||
ctl.check().map_err(|_| IoError::Cancelled)?;
|
||||
ctl.progress(1.0);
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn import_stages(name: &str, bytes: &[u8], ctl: &photocraft_raster::Interrupt) -> Result<ImportResult, IoError> {
|
||||
fn import_stages(name: &str, bytes: &[u8], ctl: &photocraft_raster::Interrupt, max_group_depth: usize) -> Result<ImportResult, IoError> {
|
||||
// A declared native extension must reach its loader so malformed bundles retain format errors.
|
||||
if has_extension(name, photocraft_format::EXTENSION) || photocraft_format::is_pcraft(bytes) {
|
||||
return Ok(ImportResult { document: photocraft_format::load_from_bytes(bytes)?, warnings: Vec::new(), source_read_only: false, preview_only: false });
|
||||
|
|
@ -212,12 +228,24 @@ fn import_stages(name: &str, bytes: &[u8], ctl: &photocraft_raster::Interrupt) -
|
|||
if raw::is_raw(bytes) {
|
||||
return raw::import_raw(name, bytes);
|
||||
}
|
||||
if has_extension(name, "svg") || has_extension(name, "svgz") || svg::is_svg(bytes) {
|
||||
return svg::import_svg(name, bytes);
|
||||
if is_svg_input(name, bytes) {
|
||||
return svg::import_svg_with_group_depth(name, bytes, max_group_depth);
|
||||
}
|
||||
flat::import_flat(name, bytes)
|
||||
}
|
||||
|
||||
/// Extension names can be wrong (for example, an image downloaded as WebP and renamed `.svg`).
|
||||
/// Valid plain SVG is recognized by its XML content; an otherwise unknown `.svg` is passed to the
|
||||
/// SVG parser for a useful malformed-file error, and `.svgz` disambiguates gzip streams.
|
||||
fn is_svg_input(name: &str, bytes: &[u8]) -> bool {
|
||||
// Keep the SVG extension as a fallback for malformed files so the SVG parser can return an
|
||||
// actionable SVG error. A recognized raster signature still wins over a misleading `.svg`
|
||||
// suffix (for example, a WebP downloaded as Firefox_logo.svg).
|
||||
svg::is_svg(bytes)
|
||||
|| (has_extension(name, "svg") && photocraft_codecs::detect(bytes).is_none())
|
||||
|| (has_extension(name, "svgz") && bytes.starts_with(&[0x1f, 0x8b]))
|
||||
}
|
||||
|
||||
fn extension(name_or_ext: &str) -> String {
|
||||
name_or_ext.rsplit(['.', '/', '\\']).next().unwrap_or(name_or_ext).to_ascii_lowercase()
|
||||
}
|
||||
|
|
@ -298,3 +326,17 @@ pub fn merged_composite(file: &PsdFile) -> Result<Vec<[f32; 4]>, IoError> {
|
|||
let s = l.surface().ok_or_else(|| IoError::Unsupported("no merged image".into()))?;
|
||||
Ok(photocraft_compose::surface_to_buffer(s, doc.bounds()).px)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::is_svg_input;
|
||||
|
||||
#[test]
|
||||
fn svg_extension_does_not_override_another_format_signature() {
|
||||
let webp_header = b"RIFF\x04\0\0\0WEBP";
|
||||
assert!(!is_svg_input("Firefox_logo,_2019.svg", webp_header));
|
||||
assert!(is_svg_input("drawing.svg", b"<?xml version=\"1.0\"?><svg/>"));
|
||||
assert!(is_svg_input("drawing.svgz", &[0x1f, 0x8b, 0, 0]));
|
||||
assert!(!is_svg_input("drawing.svgz", webp_header));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -184,13 +184,20 @@ fn canvas(tree: &usvg::Tree, warnings: &mut Vec<String>) -> Canvas {
|
|||
|
||||
/// Opens an SVG as a document of shape layers (see the module docs for what is rasterised).
|
||||
pub fn import_svg(name: &str, bytes: &[u8]) -> Result<ImportResult, IoError> {
|
||||
import_svg_with_group_depth(name, bytes, MAX_GROUP_DEPTH)
|
||||
}
|
||||
|
||||
/// Opens an SVG while rasterising any subtrees deeper than `max_group_depth` into single pixel
|
||||
/// layers. The document's hard nesting limit remains [`MAX_GROUP_DEPTH`].
|
||||
pub fn import_svg_with_group_depth(name: &str, bytes: &[u8], max_group_depth: usize) -> Result<ImportResult, IoError> {
|
||||
let tree = parse(bytes)?;
|
||||
let mut warnings = Vec::new();
|
||||
let cv = canvas(&tree, &mut warnings);
|
||||
let mut doc = Document::new(name, cv.size, ColorMode::Rgb, SampleType::U8);
|
||||
let fmt = doc.pixel_format();
|
||||
let global = tiny_skia::Transform::from_scale(cv.scale, cv.scale);
|
||||
let mut conv = Conv { fmt, canvas: doc.bounds(), global, warnings, noted: Vec::new(), counts: [0; 4] };
|
||||
let max_group_depth = (max_group_depth != 0).then_some(max_group_depth.clamp(1, MAX_GROUP_DEPTH));
|
||||
let mut conv = Conv { fmt, canvas: doc.bounds(), global, max_group_depth, warnings, noted: Vec::new(), counts: [0; 4] };
|
||||
if count_nodes(tree.root()) > MAX_NODES {
|
||||
conv.warnings.push(format!("SVG: more than {MAX_NODES} elements; opened as one pixel layer instead of shapes"));
|
||||
let (w, h) = (cv.size.width, cv.size.height);
|
||||
|
|
@ -273,6 +280,7 @@ struct Conv {
|
|||
canvas: Rect,
|
||||
/// Drawing units → canvas pixels.
|
||||
global: tiny_skia::Transform,
|
||||
max_group_depth: Option<usize>,
|
||||
warnings: Vec<String>,
|
||||
/// Features already warned about (one warning each).
|
||||
noted: Vec<&'static str>,
|
||||
|
|
@ -317,7 +325,7 @@ impl Conv {
|
|||
usvg::Node::Group(g) => {
|
||||
let dropped: [(bool, &'static str); 3] =
|
||||
[(!g.filters().is_empty(), "filter"), (g.clip_path().is_some(), "clip-path"), (g.mask().is_some(), "mask")];
|
||||
let too_deep = depth >= MAX_GROUP_DEPTH || rec >= MAX_RECURSION;
|
||||
let too_deep = self.max_group_depth.is_some_and(|limit| depth >= limit) || depth >= MAX_GROUP_DEPTH || rec >= MAX_RECURSION;
|
||||
if dropped.iter().any(|d| d.0) || too_deep {
|
||||
for (yes, what) in dropped {
|
||||
if yes {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,24 @@ fn groups_keep_opacity_blend_and_nesting_while_plain_groups_vanish() {
|
|||
assert!(p[1] > 0.9 && (p[3] - 0.5).abs() < 0.03, "{p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn svg_group_rasterization_threshold_is_configurable_and_clamped() {
|
||||
let source = format!(
|
||||
"<svg {NS} width=\"20\" height=\"20\"><g id=\"outer\"><rect width=\"2\" height=\"2\" fill=\"blue\"/><g id=\"inner\"><rect width=\"10\" height=\"10\" fill=\"red\"/><rect x=\"12\" width=\"5\" height=\"5\" fill=\"green\"/></g></g></svg>"
|
||||
);
|
||||
let shallow = import_with_svg_group_depth("nested.svg", source.as_bytes(), 1).unwrap();
|
||||
let LayerContent::Group(outer) = &shallow.document.layers[0].content else { panic!("outer group should remain editable") };
|
||||
assert!(outer.children.iter().any(|child| matches!(child.content, LayerContent::Raster(_))), "the subtree past the setting should be rasterized");
|
||||
assert!(shallow.warnings.iter().any(|warning| warning.contains("nested too deeply")));
|
||||
|
||||
let unlimited = import_with_svg_group_depth("nested.svg", source.as_bytes(), 0).unwrap();
|
||||
let LayerContent::Group(outer) = &unlimited.document.layers[0].content else { panic!("outer group should remain editable") };
|
||||
assert!(outer.children.iter().any(|child| matches!(child.content, LayerContent::Group(_))), "zero disables the preference threshold");
|
||||
|
||||
let clamped = import_with_svg_group_depth("nested.svg", source.as_bytes(), usize::MAX).unwrap();
|
||||
assert!(matches!(clamped.document.layers[0].content, LayerContent::Group(_)), "out-of-range settings clamp to the document's hard group limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transforms_curves_and_fill_rules_are_in_the_knots() {
|
||||
let r = open(&format!(
|
||||
|
|
|
|||
|
|
@ -39,7 +39,9 @@ fn main() {
|
|||
arg(&args, "--script").map(|s| serde_json::from_str::<Vec<(String, Value)>>(&s).expect("--script must be [[method, params], …]")).unwrap_or_default();
|
||||
|
||||
let services = Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| photocraft_io::import(name, bytes).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string()))),
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8], depth: usize| {
|
||||
photocraft_io::import_with_svg_group_depth(name, bytes, depth).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
|
||||
})),
|
||||
export: Some(Box::new(|doc: &photocraft_doc::Document, path: &str, settings: &photocraft_ui_egui::ExportSettings| {
|
||||
let mut opts = photocraft_io::ExportOptions::default();
|
||||
if let Some(q) = settings.jpeg_quality {
|
||||
|
|
|
|||
|
|
@ -1263,7 +1263,9 @@ mod tests {
|
|||
fn raw_app() -> (PhotocraftApp, Vec<u8>) {
|
||||
use photocraft_raw::testgen::{DngSpec, mosaic, scene};
|
||||
let services = crate::Services {
|
||||
import: Some(Box::new(|name: &str, b: &[u8]| photocraft_io::import(name, b).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string()))),
|
||||
import: Some(Box::new(|name: &str, b: &[u8], depth: usize| {
|
||||
photocraft_io::import_with_svg_group_depth(name, b, depth).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let (w, h) = (48, 32);
|
||||
|
|
|
|||
|
|
@ -1645,7 +1645,7 @@ mod tests {
|
|||
let written: Rc<RefCell<Vec<String>>> = Rc::default();
|
||||
let w = written.clone();
|
||||
let services = crate::Services {
|
||||
import: Some(Box::new(|name: &str, _b: &[u8]| {
|
||||
import: Some(Box::new(|name: &str, _b: &[u8], _depth: usize| {
|
||||
Ok((Document::new(name, Size::new(4, 4), ColorMode::Rgb, SampleType::U8), vec!["Adjustment layer flattened".to_string()]))
|
||||
})),
|
||||
export: Some(Box::new(|_d: &Document, _p: &str, _s: &crate::ExportSettings| Ok((b"out".to_vec(), vec!["Layers were flattened".to_string()])))),
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ fn app_with(answers: Vec<Option<FileDialogAnswer>>) -> (PhotocraftApp, Written)
|
|||
let written: Written = Rc::default();
|
||||
let w = written.clone();
|
||||
let services = Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8], _depth: usize| {
|
||||
if bytes == b"bad" {
|
||||
return Err("not an image".into());
|
||||
}
|
||||
|
|
@ -398,6 +398,9 @@ fn files_dropped_onto_the_canvas_are_placed_in_free_transform_one_by_one() {
|
|||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let abs = dir.join("photo.png");
|
||||
std::fs::write(&abs, png_bytes(200, 100)).unwrap();
|
||||
let svg = dir.join("logo.svg");
|
||||
std::fs::write(&svg, b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"10\"><rect width=\"20\" height=\"10\" fill=\"red\"/></svg>")
|
||||
.unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
let mut app = app_with_canvas();
|
||||
let (layers, steps) = (layer_count(&app), history_steps(&app));
|
||||
|
|
@ -439,6 +442,13 @@ fn files_dropped_onto_the_canvas_are_placed_in_free_transform_one_by_one() {
|
|||
app.place_next_dropped(&ctx);
|
||||
assert!(app.ui.transform.is_none() && app.drop_places.is_empty());
|
||||
assert!(app.ui.recent_files.is_empty(), "placing isn't opening");
|
||||
app.open_dropped(&ctx, vec![dropped(svg, Err("egui's reader must not be used".into()))], Some(egui::pos2(400.0, 300.0)));
|
||||
app.place_next_dropped(&ctx);
|
||||
let st = app.session.active().unwrap();
|
||||
let placed_svg = st.doc.layer(st.active_layer.unwrap()).unwrap();
|
||||
assert_eq!(placed_svg.name, "logo");
|
||||
assert!(matches!(placed_svg.content, photocraft_doc::LayerContent::Smart(_)), "SVG drops use Place Embedded");
|
||||
assert!(app.ui.transform.is_some(), "SVG drop should enter Free Transform");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2317,6 +2317,9 @@ fontWeight Italic Kurzíva
|
|||
Color to Alpha… Barva na alfu…
|
||||
Transparency Threshold Práh průhlednosti
|
||||
Opacity Threshold Práh krytí
|
||||
|
||||
Rasterize svg groups deeper than Rasterizovat skupiny SVG hlubší než tato úroveň
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 vypne tento práh; skupiny SVG vnořené hlouběji než 100 úrovní se rastrují vždy.
|
||||
Rule of Thirds Pravidlo třetin
|
||||
Triangle Trojúhelník
|
||||
Golden Ratio Zlatý řez
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2356,6 +2356,9 @@ fontWeight Italic Kursiv
|
|||
Color to Alpha… Farbe zu Alpha…
|
||||
Transparency Threshold Transparenz-Schwellenwert
|
||||
Opacity Threshold Deckkraft-Schwellenwert
|
||||
|
||||
Rasterize svg groups deeper than SVG-Gruppen ab dieser Tiefe rastern
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 schaltet diese Schwelle aus; SVG-Gruppen, die tiefer als 100 Ebenen verschachtelt sind, werden immer gerastert.
|
||||
Rule of Thirds Drittel-Regel
|
||||
Triangle Dreieck
|
||||
Golden Ratio Goldener Schnitt
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3428,6 +3428,9 @@ fontWeight Thin Πολύ λεπτά
|
|||
Color to Alpha… Χρώμα σε άλφα…
|
||||
Transparency Threshold Κατώφλι διαφάνειας
|
||||
Opacity Threshold Κατώφλι αδιαφάνειας
|
||||
|
||||
Rasterize svg groups deeper than Ραστεροποίηση ομάδων SVG πέρα από αυτό το βάθος
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. Το 0 απενεργοποιεί αυτό το όριο· ομάδες SVG με ένθεση βαθύτερη από 100 επίπεδα ραστεροποιούνται πάντα.
|
||||
Rule of Thirds Κανόνας των τρίτων
|
||||
Golden Ratio Χρυσή τομή
|
||||
Golden Spiral Χρυσή σπείρα
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 5.
|
|
|
@ -2385,6 +2385,9 @@ fontWeight Italic Cursiva
|
|||
Color to Alpha… Color a alfa…
|
||||
Transparency Threshold Umbral de transparencia
|
||||
Opacity Threshold Umbral de opacidad
|
||||
|
||||
Rasterize svg groups deeper than Rasterizar grupos SVG con mayor profundidad
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 desactiva este umbral; los grupos SVG anidados a más de 100 niveles siempre se rasterizan.
|
||||
Rule of Thirds Regla de los tercios
|
||||
Triangle Triángulo
|
||||
Golden Ratio Proporción áurea
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2738,6 +2738,9 @@ fontWeight Italic Italique
|
|||
Color to Alpha… Couleur vers alpha…
|
||||
Transparency Threshold Seuil de transparence
|
||||
Opacity Threshold Seuil d'opacité
|
||||
|
||||
Rasterize svg groups deeper than Pixelliser les groupes SVG au-delà de cette profondeur
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 désactive ce seuil ; les groupes SVG imbriqués sur plus de 100 niveaux sont toujours pixellisés.
|
||||
Rule of Thirds Règle des tiers
|
||||
Triangle Triangle
|
||||
Golden Ratio Nombre d’or
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2725,6 +2725,9 @@ fontWeight Italic Miring
|
|||
Color to Alpha… Warna ke Alpha…
|
||||
Transparency Threshold Ambang transparansi
|
||||
Opacity Threshold Ambang opasitas
|
||||
|
||||
Rasterize svg groups deeper than Rasterisasi grup SVG yang lebih dalam dari ini
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 menonaktifkan ambang ini; grup SVG yang bersarang lebih dari 100 tingkat selalu dirasterisasi.
|
||||
Rule of Thirds Aturan Sepertiga
|
||||
Triangle Segitiga
|
||||
Golden Ratio Rasio Emas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 4 and column 47.
|
|
|
@ -2387,6 +2387,9 @@ fontWeight Italic Corsivo
|
|||
Color to Alpha… Colore ad alfa…
|
||||
Transparency Threshold Soglia di trasparenza
|
||||
Opacity Threshold Soglia di opacità
|
||||
|
||||
Rasterize svg groups deeper than Rasterizza i gruppi SVG oltre questa profondità
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 disattiva questa soglia; i gruppi SVG annidati oltre 100 livelli vengono sempre rasterizzati.
|
||||
Rule of Thirds Regola dei terzi
|
||||
Triangle Triangolo
|
||||
Golden Ratio Sezione aurea
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -3342,6 +3342,9 @@ fontWeight Italic 斜体
|
|||
Color to Alpha… カラーを透明に…
|
||||
Transparency Threshold 透明のしきい値
|
||||
Opacity Threshold 不透明度のしきい値
|
||||
|
||||
Rasterize svg groups deeper than この深さを超える SVG グループをラスタライズ
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 でこのしきい値は無効になります。100 階層を超えて入れ子になった SVG グループは常にラスタライズされます。
|
||||
Rule of Thirds 三分割法
|
||||
Golden Ratio 黄金比
|
||||
Golden Spiral 黄金螺旋
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2897,6 +2897,9 @@ fontWeight Italic 기울임
|
|||
Color to Alpha… 색상을 알파로…
|
||||
Transparency Threshold 투명도 한계값
|
||||
Opacity Threshold 불투명도 한계값
|
||||
|
||||
Rasterize svg groups deeper than 이 깊이보다 깊은 SVG 그룹 래스터화
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0이면 이 임계값을 끕니다. 100단계보다 깊이 중첩된 SVG 그룹은 항상 래스터화됩니다.
|
||||
Rule of Thirds 삼등분 법칙
|
||||
Golden Ratio 황금 비율
|
||||
Golden Spiral 황금 나선
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 10.
|
|
|
@ -3503,6 +3503,9 @@ cameraRaw Whites Witten
|
|||
Dual Axis Dubbele as
|
||||
Diagonal Diagonaal
|
||||
Transform Symmetry Symmetrie transformeren
|
||||
|
||||
Rasterize svg groups deeper than SVG-groepen rasteriseren dieper dan deze grens
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 schakelt deze drempel uit; SVG-groepen die dieper dan 100 niveaus zijn genest, worden altijd gerasterd.
|
||||
Rule of Thirds Regel van derden
|
||||
Golden Ratio Gulden snede
|
||||
Golden Spiral Gulden spiraal
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 8 and column 34.
|
|
|
@ -3341,6 +3341,9 @@ cameraRaw Roughness Chropowatość
|
|||
Color to Alpha… Kolor na alfę…
|
||||
Transparency Threshold Próg przezroczystości
|
||||
Opacity Threshold Próg krycia
|
||||
|
||||
Rasterize svg groups deeper than Rasteryzuj grupy SVG głębsze niż ten poziom
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 wyłącza ten próg; grupy SVG zagnieżdżone głębiej niż 100 poziomów są zawsze rasteryzowane.
|
||||
Rule of Thirds Zasada trójpodziału
|
||||
Golden Ratio Złoty podział
|
||||
Golden Spiral Złota spirala
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2746,6 +2746,9 @@ fontWeight Italic Itálico
|
|||
Color to Alpha… Cor para alfa…
|
||||
Transparency Threshold Limiar de transparência
|
||||
Opacity Threshold Limiar de opacidade
|
||||
|
||||
Rasterize svg groups deeper than Rasterizar grupos SVG além desta profundidade
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 desativa este limite; grupos SVG aninhados em mais de 100 níveis são sempre rasterizados.
|
||||
Rule of Thirds Regra dos terços
|
||||
Triangle Triângulo
|
||||
Golden Ratio Proporção áurea
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -2591,6 +2591,9 @@ fontWeight Italic Курсив
|
|||
Color to Alpha… Цвет в альфа-канал…
|
||||
Transparency Threshold Порог прозрачности
|
||||
Opacity Threshold Порог непрозрачности
|
||||
|
||||
Rasterize svg groups deeper than Растеризовать группы SVG глубже этого уровня
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 отключает этот порог; группы SVG с вложенностью более 100 уровней всегда растрируются.
|
||||
Rule of Thirds Правило третей
|
||||
Triangle Треугольник
|
||||
Golden Ratio Золотое сечение
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -3428,6 +3428,9 @@ cameraRaw Masking Маскування
|
|||
Dual Axis Подвійна вісь
|
||||
Diagonal Діагональна
|
||||
Transform Symmetry Трансформувати симетрію
|
||||
|
||||
Rasterize svg groups deeper than Растеризувати групи SVG глибше цього рівня
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 вимикає цей поріг; групи SVG із вкладеністю понад 100 рівнів завжди растеризуються.
|
||||
Rule of Thirds Правило третин
|
||||
Golden Ratio Золотий перетин
|
||||
Golden Spiral Золота спіраль
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 12.
|
|
|
@ -2325,6 +2325,9 @@ fontWeight Italic 斜体
|
|||
Color to Alpha… 颜色转为 Alpha…
|
||||
Transparency Threshold 透明度阈值
|
||||
Opacity Threshold 不透明度阈值
|
||||
|
||||
Rasterize svg groups deeper than 栅格化超过此深度的 SVG 组
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 设为 0 可关闭此阈值;嵌套超过 100 层的 SVG 组始终会被栅格化。
|
||||
Rule of Thirds 三等分
|
||||
Triangle 三角形
|
||||
Golden Ratio 黄金比例
|
||||
|
|
|
|||
|
Can't render this file because it has a wrong number of fields in line 14.
|
|
|
@ -2294,6 +2294,9 @@ fontWeight Italic 斜體
|
|||
Color to Alpha… 顏色轉為 Alpha…
|
||||
Transparency Threshold 透明度臨界值
|
||||
Opacity Threshold 不透明度臨界值
|
||||
|
||||
Rasterize svg groups deeper than 點陣化超過此深度的 SVG 群組
|
||||
0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 設為 0 可關閉此臨界值;巢狀超過 100 層的 SVG 群組一律會點陣化。
|
||||
Rule of Thirds 三等分
|
||||
Triangle 三角形
|
||||
Golden Ratio 黃金比例
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -183,7 +183,7 @@ pub use file_open::OsEvent;
|
|||
pub use state::{Tool, UiState};
|
||||
|
||||
/// Decode a file: (document, warnings about anything approximated or dropped).
|
||||
pub type ImportFn = Box<dyn Fn(&str, &[u8]) -> Result<(Document, Vec<String>), String>>;
|
||||
pub type ImportFn = Box<dyn Fn(&str, &[u8], usize) -> Result<(Document, Vec<String>), String>>;
|
||||
/// Encoder settings chosen in Export As (the file format comes from the name's extension).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ExportSettings {
|
||||
|
|
@ -939,7 +939,8 @@ impl PhotocraftApp {
|
|||
return Ok(Vec::new());
|
||||
}
|
||||
let import = self.services.import.as_ref().ok_or("no importer configured")?;
|
||||
let (doc, warnings) = import(name, bytes)?;
|
||||
let max_svg_group_depth = self.session.prefs().file_handling.rasterize_svg_groups_deeper_than as usize;
|
||||
let (doc, warnings) = import(name, bytes, max_svg_group_depth)?;
|
||||
// Edit › Color Settings policies apply on open; mismatches can ask what to do.
|
||||
// No path yet: a bare name isn't a location to save back to (`open_file` sets the path).
|
||||
let (_, color) = self.session.open_document(doc, None);
|
||||
|
|
@ -988,7 +989,8 @@ impl PhotocraftApp {
|
|||
/// events and no Color Settings policy (which may read user-configured profile paths).
|
||||
fn import_automation_document(&mut self, name: &str, bytes: &[u8]) -> Result<Vec<String>, String> {
|
||||
let import = self.services.import.as_ref().ok_or("no importer configured")?;
|
||||
let (doc, warnings) = import(name, bytes)?;
|
||||
let max_svg_group_depth = self.session.prefs().file_handling.rasterize_svg_groups_deeper_than as usize;
|
||||
let (doc, warnings) = import(name, bytes, max_svg_group_depth)?;
|
||||
// The caller records the path it read from.
|
||||
self.session.add_document(doc, None);
|
||||
if let Some(st) = self.session.active_mut() {
|
||||
|
|
|
|||
|
|
@ -1694,7 +1694,9 @@ mod open_recent_tests {
|
|||
use photocraft_color::{ColorMode, SampleType};
|
||||
use photocraft_geom::Size;
|
||||
let services = crate::Services {
|
||||
import: Some(Box::new(|_n: &str, _b: &[u8]| Ok((photocraft_doc::Document::new("t", Size::new(4, 4), ColorMode::Rgb, SampleType::U8), Vec::new())))),
|
||||
import: Some(Box::new(|_n: &str, _b: &[u8], _depth: usize| {
|
||||
Ok((photocraft_doc::Document::new("t", Size::new(4, 4), ColorMode::Rgb, SampleType::U8), Vec::new()))
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), services);
|
||||
|
|
|
|||
|
|
@ -1017,6 +1017,14 @@ fn prefs_body(ui: &mut egui::Ui, f: &mut Map<String, Value>, system: Option<egui
|
|||
ui.label(RichText::new(tl!("These settings aren't available in PhotoCraft yet.")).color(t.text_faint));
|
||||
} else if let Some(obj) = values.get_mut(§ion).and_then(Value::as_object_mut) {
|
||||
section_fields(ui, §ion, obj, &order, lang, system);
|
||||
if section == "fileHandling" {
|
||||
ui.label(
|
||||
RichText::new(tl!(
|
||||
"0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized."
|
||||
))
|
||||
.color(t.text_faint),
|
||||
);
|
||||
}
|
||||
if section == "performance" {
|
||||
gpu_status_rows(ui, f.get("__gpuInfo"), obj);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ fn app() -> PhotocraftApp {
|
|||
let mut app = PhotocraftApp::new(
|
||||
Session::new(),
|
||||
Services {
|
||||
import: Some(Box::new(|name, bytes| photocraft_io::import(name, bytes).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string()))),
|
||||
import: Some(Box::new(|name, bytes, max_svg_group_depth| {
|
||||
photocraft_io::import_with_svg_group_depth(name, bytes, max_svg_group_depth).map(|r| (r.document, r.warnings)).map_err(|e| e.to_string())
|
||||
})),
|
||||
export: Some(Box::new(|doc, path, _| {
|
||||
photocraft_io::export(doc, path, &Default::default()).map(|r| (r.bytes, r.warnings)).map_err(|e| e.to_string())
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js
|
|||
| [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 15 not run, 7 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 4 | 12 | 11 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 307/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 19 | 13 | 7 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 5 | 7 | 11 | settings that do nothing: 46 of 146 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 5 | 7 | 11 | settings that do nothing: 46 of 147 (target 0) |
|
||||
| [Type](#type) | 0 | 3 | 6 | |
|
||||
| [Automation](#automation) | 3 | 2 | 6 | 627 of 627 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 0 | 2 | never-crash lints on 28 of 28 crates; panic_hunt present |
|
||||
|
|
@ -172,7 +172,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
|||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 46 of 146** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
**Settings that do nothing: 46 of 147** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't.
|
||||
|
||||
Unread: `general.color_picker`, `general.beep_when_done`, `general.resize_image_during_place`, `general.always_create_smart_objects_when_placing`, `general.animated_zoom`, `general.zoom_resizes_windows`, `interface.dynamic_color_sliders`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `tools.enable_flick_panning`, `tools.vary_round_brush_hardness_on_hud`, `tools.show_transformation_values`, `file_handling.image_previews`, `file_handling.save_in_background`, `file_handling.ignore_exif_profile_tag`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `cursors.brush_preview_color`, `units_and_rulers.type_units`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `units_and_rulers.print_resolution`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.smart_quotes`, `type_.missing_glyph_protection`, `type_.show_font_names_in_english`, `type_.text_engine`, `type_.font_preview`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `enhanced_controls.zoom_with_trackpad_pinch`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue