mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-14 19:26:51 -04:00
A file with no embedded ICC profile whose EXIF ColorSpace tag says sRGB (what cameras write) now opens as sRGB, as in Photoshop: it is tagged sRGB before the Color Settings policy runs, so it is kept, reported as a mismatch, or converted like any embedded profile. Before, it was read as untagged and took the working space, so with a wide working space such as Display P3 its colours were reinterpreted. Preferences › File Handling › Ignore EXIF Profile Tag (off by default) restores the untagged reading, and is now shown in the dialog. photocraft_codecs::exif_color_space reads the tag from the Exif sub-IFD, bounds-checked; anything malformed reads as no tag. Settings that do nothing: 43 → 42 of 150. Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
5 files changed
+233
-6
No files matched your search
@@ -0,0 +1,138 @@
|
||||
//! The EXIF ColorSpace tag (0xA001, in the Exif sub-IFD that IFD0's tag 0x8769 points to):
|
||||
//! 1 = sRGB, 0xFFFF = uncalibrated (EXIF 2.3, §4.6.5).
|
||||
//!
|
||||
//! Cameras write it instead of an ICC profile. Photoshop opens a file with no embedded profile but
|
||||
//! ColorSpace = sRGB as sRGB, unless Preferences › File Handling › Ignore EXIF Profile Tag is on.
|
||||
//! Anything malformed, truncated or missing reads as no tag.
|
||||
|
||||
use crate::orientation::{Order, ifd0_entries, tiff_body};
|
||||
|
||||
/// IFD0's pointer to the Exif sub-IFD.
|
||||
const TAG_EXIF_IFD: u16 = 0x8769;
|
||||
/// The ColorSpace tag in the Exif sub-IFD.
|
||||
const TAG_COLOR_SPACE: u16 = 0xA001;
|
||||
const TYPE_SHORT: u16 = 3;
|
||||
const TYPE_LONG: u16 = 4;
|
||||
/// TIFF type 13: an IFD offset (some writers use it for sub-IFD pointers).
|
||||
const TYPE_IFD: u16 = 13;
|
||||
/// Bytes per classic TIFF IFD entry; EXIF is never BigTIFF.
|
||||
const ENTRY: usize = 12;
|
||||
/// No IFD can start inside the 8-byte TIFF header.
|
||||
const HEADER: usize = 8;
|
||||
|
||||
/// The ColorSpace value meaning sRGB.
|
||||
pub const EXIF_COLOR_SPACE_SRGB: u16 = 1;
|
||||
|
||||
/// The EXIF ColorSpace value of a TIFF-structured block (an EXIF payload with or without the
|
||||
/// `Exif\0\0` prefix), or `None` when it has none or it is malformed.
|
||||
pub fn exif_color_space(exif: &[u8]) -> Option<u16> {
|
||||
let b = tiff_body(exif);
|
||||
let (order, first, count, entry_bytes, big_tiff) = ifd0_entries(b)?;
|
||||
if big_tiff {
|
||||
return None;
|
||||
}
|
||||
let ptr = find(order, b, first, count, entry_bytes, TAG_EXIF_IFD)?;
|
||||
if !matches!(order.u16(b, ptr.checked_add(2)?)?, TYPE_LONG | TYPE_IFD) || order.u32(b, ptr.checked_add(4)?)? != 1 {
|
||||
return None;
|
||||
}
|
||||
let ifd = usize::try_from(order.u32(b, ptr.checked_add(8)?)?).ok()?;
|
||||
if ifd < HEADER {
|
||||
return None;
|
||||
}
|
||||
let n = usize::from(order.u16(b, ifd)?);
|
||||
let first = ifd.checked_add(2)?;
|
||||
// The whole table must be present.
|
||||
if first.checked_add(n.checked_mul(ENTRY)?)? > b.len() {
|
||||
return None;
|
||||
}
|
||||
let e = find(order, b, first, n, ENTRY, TAG_COLOR_SPACE)?;
|
||||
if order.u32(b, e.checked_add(4)?)? != 1 {
|
||||
return None;
|
||||
}
|
||||
match order.u16(b, e.checked_add(2)?)? {
|
||||
TYPE_SHORT => order.u16(b, e.checked_add(8)?),
|
||||
TYPE_LONG => u16::try_from(order.u32(b, e.checked_add(8)?)?).ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset of the first entry tagged `tag` in the IFD whose `count` entries start at `first`.
|
||||
fn find(order: Order, b: &[u8], first: usize, count: usize, entry_bytes: usize, tag: u16) -> Option<usize> {
|
||||
(0..count).filter_map(|i| first.checked_add(i.checked_mul(entry_bytes)?)).find(|&e| order.u16(b, e) == Some(tag))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A little- or big-endian EXIF block: IFD0 holding only the Exif pointer, then an Exif IFD
|
||||
/// holding `entries` (tag, type, count, value).
|
||||
fn exif(big_endian: bool, entries: &[(u16, u16, u32, u32)]) -> Vec<u8> {
|
||||
let u16b = |v: u16| if big_endian { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
let u32b = |v: u32| if big_endian { v.to_be_bytes() } else { v.to_le_bytes() };
|
||||
let mut b = if big_endian { b"MM\0\x2a".to_vec() } else { b"II\x2a\0".to_vec() };
|
||||
b.extend(u32b(8));
|
||||
// IFD0: one entry, then the next-IFD pointer. The Exif IFD starts right after (8 + 2 + 12 + 4).
|
||||
b.extend(u16b(1));
|
||||
b.extend(u16b(TAG_EXIF_IFD));
|
||||
b.extend(u16b(TYPE_LONG));
|
||||
b.extend(u32b(1));
|
||||
b.extend(u32b(26));
|
||||
b.extend(u32b(0));
|
||||
b.extend(u16b(entries.len() as u16));
|
||||
for &(tag, ty, count, value) in entries {
|
||||
b.extend(u16b(tag));
|
||||
b.extend(u16b(ty));
|
||||
b.extend(u32b(count));
|
||||
// SHORT values are left-justified in the 4-byte field.
|
||||
if ty == TYPE_SHORT {
|
||||
b.extend(u16b(value as u16));
|
||||
b.extend([0, 0]);
|
||||
} else {
|
||||
b.extend(u32b(value));
|
||||
}
|
||||
}
|
||||
b.extend(u32b(0));
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_the_color_space_in_either_byte_order() {
|
||||
for big in [false, true] {
|
||||
let srgb = exif(big, &[(0x9000, 7, 4, 0x3032_3330), (TAG_COLOR_SPACE, TYPE_SHORT, 1, 1)]);
|
||||
assert_eq!(exif_color_space(&srgb), Some(EXIF_COLOR_SPACE_SRGB), "big endian: {big}");
|
||||
let mut prefixed = b"Exif\0\0".to_vec();
|
||||
prefixed.extend(&srgb);
|
||||
assert_eq!(exif_color_space(&prefixed), Some(1));
|
||||
assert_eq!(exif_color_space(&exif(big, &[(TAG_COLOR_SPACE, TYPE_SHORT, 1, 0xFFFF)])), Some(0xFFFF));
|
||||
assert_eq!(exif_color_space(&exif(big, &[(TAG_COLOR_SPACE, TYPE_LONG, 1, 1)])), Some(1), "LONG is accepted");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_malformed_reads_as_no_tag() {
|
||||
assert_eq!(exif_color_space(&exif(false, &[(0x9000, 7, 4, 0)])), None, "no ColorSpace entry");
|
||||
assert_eq!(exif_color_space(&exif(false, &[(TAG_COLOR_SPACE, TYPE_SHORT, 2, 1)])), None, "count ≠ 1");
|
||||
assert_eq!(exif_color_space(&exif(false, &[(TAG_COLOR_SPACE, 2, 1, 1)])), None, "ASCII type");
|
||||
assert_eq!(exif_color_space(&exif(false, &[(TAG_COLOR_SPACE, TYPE_LONG, 1, 0x1_0000)])), None, "LONG over u16");
|
||||
let good = exif(false, &[(TAG_COLOR_SPACE, TYPE_SHORT, 1, 1)]);
|
||||
// Every truncation is rejected without panicking.
|
||||
for len in 0..good.len() - 4 {
|
||||
assert_eq!(exif_color_space(&good[..len]), None, "truncated to {len}");
|
||||
}
|
||||
// An Exif pointer past the end, into the header, or of the wrong type.
|
||||
for (at, value) in [(18usize, 0xFFFF_FFF0u32), (18, 4), (18, u32::MAX)] {
|
||||
let mut b = good.clone();
|
||||
b[at..at + 4].copy_from_slice(&value.to_le_bytes());
|
||||
assert_eq!(exif_color_space(&b), None, "pointer {value:#x}");
|
||||
}
|
||||
let mut b = good.clone();
|
||||
b[12..14].copy_from_slice(&TYPE_SHORT.to_le_bytes());
|
||||
assert_eq!(exif_color_space(&b), None, "pointer type SHORT");
|
||||
// An Exif IFD claiming more entries than the block holds.
|
||||
let mut b = good;
|
||||
b[26..28].copy_from_slice(&u16::MAX.to_le_bytes());
|
||||
assert_eq!(exif_color_space(&b), None);
|
||||
assert_eq!(exif_color_space(b"not exif"), None);
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
mod codecs;
|
||||
mod error;
|
||||
pub mod exif_color;
|
||||
mod fidelity;
|
||||
mod format;
|
||||
mod image;
|
||||
@@ -28,6 +29,7 @@ pub use crate::codecs::png::encode_indexed as encode_png_indexed;
|
||||
pub use crate::codecs::tiff::{PhotoshopTags as TiffPhotoshopTags, photoshop_tags as tiff_photoshop_tags, writes_little_endian as tiff_writes_little_endian};
|
||||
pub use crate::codecs::tiff_ifd::{TiffInfo, TiffPage, TiffPageKind};
|
||||
pub use crate::error::CodecError;
|
||||
pub use crate::exif_color::{EXIF_COLOR_SPACE_SRGB, exif_color_space};
|
||||
pub use crate::fidelity::{FidelityWarning, fidelity_warnings, fidelity_warnings_with};
|
||||
pub use crate::format::{ASYMMETRIC_EXCEPTIONS, Format, FormatCaps, caps, detect, from_extension};
|
||||
pub use crate::image::{
|
||||
|
||||
@@ -238,11 +238,28 @@ impl ColorState {
|
||||
|
||||
/// Apply the colour management policy to a document being opened. Returns what happened
|
||||
/// (`action`: kept|converted|discarded|assigned|untagged) and whether to ask the user.
|
||||
pub fn open_policy(&self, doc: &mut Document) -> Value {
|
||||
///
|
||||
/// An RGB file with no embedded profile whose EXIF ColorSpace tag says sRGB (as cameras write)
|
||||
/// is opened as sRGB, as in Photoshop; `ignore_exif` (Preferences › File Handling › Ignore
|
||||
/// EXIF Profile Tag) treats it as untagged instead. The report then has `exif: true`.
|
||||
pub fn open_policy(&self, doc: &mut Document, ignore_exif: bool) -> Value {
|
||||
let space = mode_space(doc.mode);
|
||||
if !matches!(space, ColorSpace::Rgb | ColorSpace::Cmyk | ColorSpace::Gray) {
|
||||
return json!({"action": "kept"});
|
||||
}
|
||||
let exif_srgb = space == ColorSpace::Rgb
|
||||
&& !ignore_exif
|
||||
&& doc.icc_profile.is_none()
|
||||
&& doc.metadata.exif.as_deref().and_then(|e| photocraft_codecs::exif_color_space(e)) == Some(photocraft_codecs::EXIF_COLOR_SPACE_SRGB);
|
||||
if exif_srgb {
|
||||
doc.icc_profile = Some(Builtin::Srgb.profile().to_bytes());
|
||||
}
|
||||
let report = self.apply_open_policy(doc, space);
|
||||
if exif_srgb { merge(report, json!({"exif": true})) } else { report }
|
||||
}
|
||||
|
||||
/// [`ColorState::open_policy`] once the document's profile is known.
|
||||
fn apply_open_policy(&self, doc: &mut Document, space: ColorSpace) -> Value {
|
||||
let working = self.working(doc.mode);
|
||||
let policy = self.settings.policy(doc.mode);
|
||||
let embedded = doc.icc_profile.as_ref().and_then(|b| profile_from_bytes(b).ok()).filter(|p| p.color_space == space);
|
||||
@@ -1012,7 +1029,7 @@ impl Session {
|
||||
/// convert to the working space, or discard the embedded profile). Returns the document
|
||||
/// index and a report (`action`, `mismatch`, `ask`) the UI can turn into a prompt.
|
||||
pub fn open_document(&mut self, mut doc: Document, path: Option<String>) -> (usize, Value) {
|
||||
let report = self.color.open_policy(&mut doc);
|
||||
let report = self.color.open_policy(&mut doc, self.prefs().file_handling.ignore_exif_profile_tag);
|
||||
(self.add_document(doc, path), report)
|
||||
}
|
||||
}
|
||||
@@ -1380,6 +1397,76 @@ mod settings_tests {
|
||||
assert_eq!(desc(&s.active().unwrap().doc).as_deref(), Some("Display P3"));
|
||||
}
|
||||
|
||||
/// A little-endian EXIF block whose Exif IFD holds ColorSpace = `space`.
|
||||
fn exif_color_space(space: u16) -> Vec<u8> {
|
||||
let mut b = b"Exif\0\0II\x2a\0\x08\0\0\0".to_vec();
|
||||
// IFD0: the Exif IFD pointer (LONG) to offset 26, then no next IFD.
|
||||
b.extend([1, 0, 0x69, 0x87, 4, 0, 1, 0, 0, 0, 26, 0, 0, 0, 0, 0, 0, 0]);
|
||||
// Exif IFD: ColorSpace (SHORT).
|
||||
b.extend([1, 0, 0x01, 0xA0, 3, 0, 1, 0, 0, 0]);
|
||||
b.extend(space.to_le_bytes());
|
||||
b.extend([0, 0, 0, 0, 0, 0]);
|
||||
b
|
||||
}
|
||||
|
||||
fn untagged_camera_file(space: Option<u16>) -> Document {
|
||||
let mut d = tagged("srgb");
|
||||
d.icc_profile = None;
|
||||
d.metadata.exif = space.map(|v| Arc::new(exif_color_space(v)));
|
||||
d
|
||||
}
|
||||
|
||||
/// The tag survives a real JPEG: written without an ICC profile, read back, opened as sRGB.
|
||||
#[test]
|
||||
fn exif_srgb_jpeg_round_trip() {
|
||||
let mut s = Session::new();
|
||||
s.execute("edit.colorSettings", json!({"workingRgb": "display-p3"})).unwrap();
|
||||
let r = photocraft_io::export(&untagged_camera_file(Some(1)), "x.jpg", &photocraft_io::ExportOptions::default()).unwrap();
|
||||
let back = photocraft_io::import("x.jpg", &r.bytes).unwrap().document;
|
||||
assert!(back.icc_profile.is_none(), "no ICC profile in the file");
|
||||
let (_, r) = s.open_document(back, None);
|
||||
assert_eq!((r["action"].as_str(), r["exif"].as_bool()), (Some("kept"), Some(true)));
|
||||
assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
|
||||
}
|
||||
|
||||
/// #204: a camera file with no ICC profile but EXIF ColorSpace = sRGB opens as sRGB, as in
|
||||
/// Photoshop; Ignore EXIF Profile Tag opens it untagged (the working space).
|
||||
#[test]
|
||||
fn exif_srgb_tag_unless_ignored() {
|
||||
let mut s = Session::new();
|
||||
// Working sRGB: tagged sRGB, nothing to ask.
|
||||
let (_, r) = s.open_document(untagged_camera_file(Some(1)), None);
|
||||
assert_eq!((r["action"].as_str(), r["exif"].as_bool(), r.get("ask")), (Some("kept"), Some(true), None));
|
||||
assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
|
||||
// Working Display P3, Preserve: it stays sRGB instead of being read as P3.
|
||||
s.execute("edit.colorSettings", json!({"policyRgb": "preserve", "workingRgb": "display-p3"})).unwrap();
|
||||
let (_, r) = s.open_document(untagged_camera_file(Some(1)), None);
|
||||
assert_eq!((r["action"].as_str(), r["mismatch"].as_bool(), r["exif"].as_bool()), (Some("kept"), Some(true), Some(true)));
|
||||
assert!(desc(&s.active().unwrap().doc).unwrap().starts_with("sRGB"));
|
||||
// Convert: converted from sRGB to P3, so the pixel values change.
|
||||
s.execute("edit.colorSettings", json!({"policyRgb": "convert"})).unwrap();
|
||||
let (_, r) = s.open_document(untagged_camera_file(Some(1)), None);
|
||||
assert_eq!(r["action"], "converted");
|
||||
let d = &s.active().unwrap().doc;
|
||||
assert_eq!(desc(d).as_deref(), Some("Display P3"));
|
||||
assert!((d.layers[0].surface().unwrap().pixel(1, 1)[0] - 0.2).abs() > 0.01);
|
||||
// Uncalibrated, no EXIF, or Ignore EXIF Profile Tag on: untagged, so the working space.
|
||||
s.execute("edit.colorSettings", json!({"policyRgb": "preserve"})).unwrap();
|
||||
for (space, ignore) in [(Some(0xFFFF), false), (None, false), (Some(1), true)] {
|
||||
s.execute("prefs.set", json!({"path": "fileHandling.ignoreExifProfileTag", "value": ignore})).unwrap();
|
||||
let (_, r) = s.open_document(untagged_camera_file(space), None);
|
||||
assert_eq!((r["action"].as_str(), r.get("exif")), (Some("assigned"), None), "{space:?}, ignore {ignore}");
|
||||
assert_eq!(desc(&s.active().unwrap().doc).as_deref(), Some("Display P3"));
|
||||
}
|
||||
// An embedded profile always wins over the EXIF tag.
|
||||
s.execute("prefs.set", json!({"path": "fileHandling.ignoreExifProfileTag", "value": false})).unwrap();
|
||||
let mut d = tagged("adobe-rgb-compat");
|
||||
d.metadata.exif = Some(Arc::new(exif_color_space(1)));
|
||||
let (_, r) = s.open_document(d, None);
|
||||
assert_eq!((r["action"].as_str(), r.get("exif")), (Some("kept"), None));
|
||||
assert_eq!(desc(&s.active().unwrap().doc), Some(Builtin::from_id("adobe-rgb-compat").unwrap().profile().description.clone()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_conversion_uses_working_spaces() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -476,6 +476,7 @@ pub struct FileHandling {
|
||||
pub autosave_minutes: u32,
|
||||
/// Offer to reopen recovered documents at launch.
|
||||
pub recover_on_launch: bool,
|
||||
/// Open a file with no ICC profile as untagged even when its EXIF ColorSpace tag says sRGB.
|
||||
pub ignore_exif_profile_tag: bool,
|
||||
pub ask_before_saving_layered_tiff: bool,
|
||||
pub maximize_psd_compatibility: Ask,
|
||||
@@ -999,7 +1000,6 @@ pub const HIDDEN_UNTIL_IMPLEMENTED: &[&str] = &[
|
||||
"workspace.enableFloatingDocumentWindowDocking",
|
||||
"workspace.enableNarrowOptionsBar",
|
||||
"fileHandling.saveInBackground",
|
||||
"fileHandling.ignoreExifProfileTag",
|
||||
"fileHandling.maximizePsdCompatibility",
|
||||
"performance.cacheLevels",
|
||||
"performance.effectCacheMb",
|
||||
|
||||
+3
-3
@@ -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, 17 not run, 5 not measurable yet (baseline 2026-10-05) |
|
||||
| [File compatibility](#file-compatibility) | 6 | 12 | 9 | io corpus: oracle ≥ 146/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 236/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 133/256, round trip ≥ 258/256 |
|
||||
| [Tools](#tools) | 25 | 16 | 2 | |
|
||||
| [Workspace and UI](#workspace-and-ui) | 9 | 8 | 6 | settings that do nothing: 31 of 154 (target 0) |
|
||||
| [Workspace and UI](#workspace-and-ui) | 9 | 8 | 6 | settings that do nothing: 30 of 154 (target 0) |
|
||||
| [Type](#type) | 3 | 3 | 3 | |
|
||||
| [Automation](#automation) | 3 | 2 | 6 | 631 of 631 menu items live (100.0%) |
|
||||
| [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 28 of 28 crates; panic_hunt present |
|
||||
@@ -176,9 +176,9 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t
|
||||
|
||||
## Workspace and UI
|
||||
|
||||
**Settings that do nothing: 31 of 154** (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: 30 of 154** (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.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `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`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `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`.
|
||||
Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_collapse_icon_panels`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `workspace.enable_narrow_options_bar`, `file_handling.save_in_background`, `file_handling.maximize_psd_compatibility`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.missing_glyph_protection`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `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`.
|
||||
|
||||
| Id | Target | Status | Issue | Note |
|
||||
|---|---|---|---|---|
|
||||
|
||||
Reference in new issue
Block a user