From aa4d4859b4912a4efc960fbe6ca703d227477a09 Mon Sep 17 00:00:00 2001 From: Sir Starch Date: Mon, 12 Oct 2026 15:13:38 +0200 Subject: [PATCH 01/27] fix(io): preserve indexed BMP color tables (#3802) (#3869) Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/codecs/README.md | 3 ++ crates/codecs/src/codecs/via_image.rs | 5 ++++ crates/codecs/src/lib.rs | 5 ++++ crates/io/src/flat.rs | 8 +++++ crates/io/tests/flat.rs | 43 +++++++++++++++++++++++++++ docs/file-format-parity.md | 5 ++-- 6 files changed, 67 insertions(+), 2 deletions(-) diff --git a/crates/codecs/README.md b/crates/codecs/README.md index ee639bcb17..e44890a0d9 100644 --- a/crates/codecs/README.md +++ b/crates/codecs/README.md @@ -61,6 +61,9 @@ the encode plan, and `fidelity_warnings` reports the conversion when it loses in `Image::convert` applies the same ICC rule: it keeps the profile only when the colour model stays the same. +Palette BMP pixels decode as RGB in `Image`; `bmp_palette` exposes the embedded table so +`photocraft-io` can open them in Indexed Color mode. + ## Documented asymmetries and limitations * **FITS.** The image in the primary HDU, or the first `IMAGE` extension, is read; `NAXIS3 = 3` diff --git a/crates/codecs/src/codecs/via_image.rs b/crates/codecs/src/codecs/via_image.rs index eca2fbb748..c91be5baf7 100644 --- a/crates/codecs/src/codecs/via_image.rs +++ b/crates/codecs/src/codecs/via_image.rs @@ -32,6 +32,11 @@ fn map_err(f: Format, e: image::ImageError) -> CodecError { } } +pub(crate) fn bmp_palette(bytes: &[u8]) -> Result>, CodecError> { + let decoder = image::codecs::bmp::BmpDecoder::new(Cursor::new(bytes)).map_err(|e| map_err(Format::Bmp, e))?; + Ok(decoder.get_palette().map(|palette| palette.to_vec())) +} + pub(crate) fn decode(f: Format, bytes: &[u8], limits: &Limits) -> Result { let fmt = image_format(f).ok_or_else(|| CodecError::unsupported(f, "not handled by image backend"))?; let mut reader = image::ImageReader::with_format(Cursor::new(bytes), fmt); diff --git a/crates/codecs/src/lib.rs b/crates/codecs/src/lib.rs index 8278ac0d6b..16025a5a46 100644 --- a/crates/codecs/src/lib.rs +++ b/crates/codecs/src/lib.rs @@ -98,6 +98,11 @@ pub fn decode_as_with(format: Format, bytes: &[u8], opts: &DecodeOptions) -> Res finish_decode(img, o, opts) } +/// Returns the embedded colour table for an indexed BMP, if it has one. +pub fn bmp_palette(bytes: &[u8]) -> Result>, CodecError> { + via_image::bmp_palette(bytes) +} + /// Lists the pages (image directories) of a TIFF or BigTIFF file without decoding pixels: the /// main IFD chain and its SubIFDs, each with its size, kind (page, reduced-resolution copy or /// mask) and storage. [`TiffInfo::default_page`] is the page [`decode`] opens. diff --git a/crates/io/src/flat.rs b/crates/io/src/flat.rs index 450969e827..129282e61b 100644 --- a/crates/io/src/flat.rs +++ b/crates/io/src/flat.rs @@ -20,7 +20,15 @@ pub fn import_flat(name: &str, bytes: &[u8]) -> Result { return import_tiff_page(name, bytes, None); } let img = codecs::decode(bytes)?; + // A palette BMP opens in Indexed Color mode with its colour table, as in Photoshop (#3802). + let bmp_palette = if codecs::detect(bytes) == Some(Format::Bmp) { codecs::bmp_palette(bytes).ok().flatten() } else { None }; let mut r = image_to_document(name, &img)?; + if let Some(colors) = bmp_palette.filter(|c| !c.is_empty()) + && r.document.mode == ColorMode::Rgb + { + r.document.mode = ColorMode::Indexed; + r.document.color_table = Some(photocraft_doc::ColorTable { colors, transparent: None }); + } r.document.file_encoding = file_encoding(bytes); if codecs::detect(bytes) == Some(Format::Tga) && img.layout() == ChannelLayout::Rgba { tga_alpha_to_channel(&mut r.document)?; diff --git a/crates/io/tests/flat.rs b/crates/io/tests/flat.rs index 728f65d1f2..4b184ad7fa 100644 --- a/crates/io/tests/flat.rs +++ b/crates/io/tests/flat.rs @@ -54,6 +54,49 @@ codec_rt!(fits_rgb8, "fits", ColorMode::Rgb, SampleType::U8, false, 0.0); codec_rt!(fits_gray16, "fit", ColorMode::Grayscale, SampleType::U16, false, 0.0); codec_rt!(fits_rgb32, "fits", ColorMode::Rgb, SampleType::F32, false, 0.0); +fn indexed_bmp_8() -> Vec { + const FILE_HEADER_SIZE: usize = 14; + const INFO_HEADER_SIZE: usize = 40; + const PALETTE_ENTRY_SIZE: usize = 4; + const PALETTE: [[u8; 3]; 2] = [[10, 20, 30], [200, 100, 50]]; + const PIXEL_ROW: [u8; 4] = [0, 1, 0, 0]; + let pixel_offset = FILE_HEADER_SIZE + INFO_HEADER_SIZE + PALETTE.len() * PALETTE_ENTRY_SIZE; + let file_size = pixel_offset + PIXEL_ROW.len(); + let mut bytes = Vec::with_capacity(file_size); + bytes.extend_from_slice(b"BM"); + bytes.extend_from_slice(&(file_size as u32).to_le_bytes()); + bytes.extend_from_slice(&[0; 4]); + bytes.extend_from_slice(&(pixel_offset as u32).to_le_bytes()); + bytes.extend_from_slice(&(INFO_HEADER_SIZE as u32).to_le_bytes()); + bytes.extend_from_slice(&2i32.to_le_bytes()); + bytes.extend_from_slice(&1i32.to_le_bytes()); + bytes.extend_from_slice(&1u16.to_le_bytes()); + bytes.extend_from_slice(&8u16.to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + bytes.extend_from_slice(&(PIXEL_ROW.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&0i32.to_le_bytes()); + bytes.extend_from_slice(&0i32.to_le_bytes()); + bytes.extend_from_slice(&(PALETTE.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&0u32.to_le_bytes()); + for [r, g, b] in PALETTE { + bytes.extend_from_slice(&[b, g, r, 0]); + } + bytes.extend_from_slice(&PIXEL_ROW); + bytes +} + +#[test] +fn indexed_bmp_opens_with_its_color_table() { + let doc = import("indexed.bmp", &indexed_bmp_8()).unwrap().document; + assert_eq!(doc.mode, ColorMode::Indexed); + assert_eq!(doc.depth, SampleType::U8); + assert_eq!(doc.color_table.as_ref().unwrap().colors.get(..2), Some(&[[10, 20, 30], [200, 100, 50]][..])); + let px = doc.layers[0].surface().unwrap().rgba(1, 0); + assert!((px[0] - 200.0 / 255.0).abs() < 1e-6); + assert!((px[1] - 100.0 / 255.0).abs() < 1e-6); + assert!((px[2] - 50.0 / 255.0).abs() < 1e-6); +} + /// FITS header cards travel as the document's text metadata, so an astro frame keeps its /// OBJECT, EXPTIME and the rest through an edit. #[test] diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index deb414961d..024d596f1e 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Save keeps a JPEG's quality and a WebP's lossy/lossless encoding) · **Target:** Adobe Photoshop 2026 +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (indexed BMP palette mode; Save keeps a JPEG's quality and a WebP's encoding) · **Target:** Adobe Photoshop 2026 Every format Photoshop 2026 (27.11.0) reads or writes, with PhotoCraft's support, fidelity and tests. Photoshop's list comes from its `Info.plist` `CFBundleDocumentTypes`, the PiPL read/write @@ -28,7 +28,7 @@ XL are missing (gap G10). Remaining: 300–500 h including PSD fidelity. | JPEG | R/W | yes | yes | 8-bit Gray/RGB/CMYK, ICC/EXIF/XMP; Save keeps the opened file's quality (estimated from its quantisation tables, #2399); no JPEG Options dialog (quality 0–12, baseline/progressive) (FILE-215-19) | | PNG | R/W | yes | yes | 8/16-bit, ICC/text; APNG first frame only; alpha channels on open (#2225) | | GIF | R/W | yes | yes | File › Open keeps the first frame; New Video Layer from File and Video Frames to Layers take every frame, with the timeline rate from the delays (#572). Per-frame delays and keeping the frames in `.pcraft` are not done | -| BMP | R/W | yes | yes | 8-bit | +| BMP | R/W | yes | yes | 8-bit; palette BMPs open in Indexed Color mode | | WebP | R/W | yes | yes | Lossless and our own lossy VP8 encoder; ICC/EXIF/XMP; Save keeps an opened file lossy (at its quality) or lossless (#2399) | | AVIF | R/W | no (#463) | optional feature, off by default | | | JPEG XL | R/W | inside DNG only | no | | @@ -140,6 +140,7 @@ remaining gaps in #1606 stay tracked with per-sample corpus ceilings. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | Palette BMPs open in Indexed Color mode (#3802) | | 2026-10-12 | minor | Save writes a JPEG at its own estimated quality and a WebP lossy or lossless as opened (#2399) | | 2026-10-11 | minor | D5100 image area and fitted-look preservation in Camera Raw tuning | | 2026-10-10 | major | First version: Photoshop 2026 27.11.0's formats from its bundle, against our codecs; Affinity notes moved from `roadmap.md` | From 8d49ecf50d59885fb2dcf38a203d62b314565726 Mon Sep 17 00:00:00 2001 From: FinnNk Date: Mon, 12 Oct 2026 14:17:43 +0100 Subject: [PATCH 02/27] feat: Photoshop's tool keys (arrows turn the brush, Shift-click new type, Space+F canvas colour) (#3725) * feat: Left and Right arrows turn the brush tip With a painting tool, Photoshop's Left and Right arrow keys turn the brush tip 1 degree (15 with Shift); Right turns it clockwise. tools.rotateBrush turns the session brush's angle by "degrees", wrapped into -180..180, and move_mods::arrow_keys gives it the plain and Shift arrows only for the painting tools, when no Move-tool, transform or selection nudge applies and no type is being edited. Alt-arrows still kern type; Ctrl-arrows still nudge the layer. Co-Authored-By: Claude Opus 5.5 * feat(ui): Shift-click with the Type tool starts new text over existing type In Photoshop a click on existing type with the Type tool edits it, and a Shift-click there starts a new type layer instead, so text can be placed over other text. pointer_down now leaves a Shift-click unconsumed when no type is being edited, so it starts new text; inside the text being edited, Shift-click still extends the selection. Co-Authored-By: Claude Opus 5.5 * feat(ui): Space+F cycles the canvas colour Photoshop's Space+F steps the pasteboard (canvas) colour through Default, Black, Dark Gray, Medium Gray, Light Gray and Custom, and Shift+Space+F steps back, round and round. While the Hand key is held, F does this instead of Cycle Screen Mode; the choice is saved like picking it in the pasteboard menu (interface.canvasColor). docs/ui-parity.md lists the new tool keys. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- crates/engine/src/brush_key_cmds.rs | 48 ++++++++++++++++++++- crates/ui-egui/src/canvas_tool_menu.rs | 14 ++++++ crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/move_mods.rs | 52 ++++++++++++++++++++++- crates/ui-egui/src/shortcuts.rs | 11 +++++ crates/ui-egui/src/tool_shortcut_tests.rs | 39 +++++++++++++++++ crates/ui-egui/src/type_tool.rs | 21 ++++++++- docs/ui-parity.md | 4 +- 8 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 crates/ui-egui/src/tool_shortcut_tests.rs diff --git a/crates/engine/src/brush_key_cmds.rs b/crates/engine/src/brush_key_cmds.rs index e4887bee39..56663b400a 100644 --- a/crates/engine/src/brush_key_cmds.rs +++ b/crates/engine/src/brush_key_cmds.rs @@ -2,11 +2,15 @@ //! has no menu item for them but lists them in Edit › Keyboard Shortcuts › Tools ("Decrease Brush //! Size"…); here they are ordinary commands with default shortcuts, so they can be rebound, //! recorded and driven by agents. Each one is a `tools.setBrush` on the session brush. +//! +//! `tools.rotateBrush` turns the tip: Photoshop's ← / → while painting (1°, ⇧ 15°). Arrows mean +//! other things with other tools, so the shell gives them to it only for the painting tools +//! (`move_mods::arrow_keys`). use serde_json::{Value, json}; use crate::commands::{CommandSpec, always}; -use crate::{Result, Session}; +use crate::{EngineError, Result, Session}; /// Photoshop's largest brush diameter, in pixels. pub const MAX_SIZE: f32 = 5000.0; @@ -35,6 +39,23 @@ fn hardness(s: &mut Session, up: bool) -> Result { crate::brush_cmds::set_brush(s, &json!({ "brush": { "hardness": hardness } })) } +/// The brush angle turned by `degrees` (positive is anticlockwise, as the angle field counts), +/// wrapped into Photoshop's -180..=180. +fn turned(angle: f32, degrees: f32) -> f32 { + let a = (angle + degrees).rem_euclid(360.0); + if a > 180.0 { a - 360.0 } else { a } +} + +fn rotate(s: &mut Session, p: &Value) -> Result { + let by = p + .get("degrees") + .and_then(Value::as_f64) + .filter(|d| d.is_finite() && d.abs() <= 360.0) + .ok_or_else(|| EngineError::BadParams { cmd: "tools.rotateBrush".into(), msg: "`degrees` must be a number from -360 to 360".into() })?; + let angle = turned(s.tools.brush.angle, by as f32); + crate::brush_cmds::set_brush(s, &json!({ "brush": { "angle": angle } })) +} + macro_rules! key_cmd { ($id:literal, $label:literal, $sc:literal, $run:expr) => { CommandSpec { id: $id, label: $label, menu: &[], shortcut: Some($sc), params: "{}", enabled: always, run: $run, journal: true } @@ -47,6 +68,16 @@ pub fn specs() -> Vec { key_cmd!("tools.increaseBrushSize", "Increase Brush Size", "]", |s, _| size(s, true)), key_cmd!("tools.decreaseBrushHardness", "Decrease Brush Hardness", "Shift+[", |s, _| hardness(s, false)), key_cmd!("tools.increaseBrushHardness", "Increase Brush Hardness", "Shift+]", |s, _| hardness(s, true)), + CommandSpec { + id: "tools.rotateBrush", + label: "Rotate Brush Tip", + menu: &[], + shortcut: None, + params: r##"{"degrees":-360..360 (positive anticlockwise; ← / → while painting send 1, ⇧ 15)}"##, + enabled: always, + run: rotate, + journal: true, + }, ] } @@ -74,6 +105,21 @@ mod tests { assert_eq!(hardness_step(0.0, false), 0.0); } + #[test] + fn rotate_brush_turns_the_tip_and_wraps_at_180() { + assert_eq!(turned(0.0, 1.0), 1.0); + assert_eq!(turned(179.0, 15.0), -166.0); + assert_eq!(turned(-175.0, -15.0), 170.0); + assert_eq!(turned(180.0, 0.0), 180.0); + let mut s = Session::new(); + s.execute("tools.rotateBrush", json!({"degrees": -15})).unwrap(); + assert_eq!(s.tools.brush.angle, -15.0); + for bad in [json!({}), json!({"degrees": "x"}), json!({"degrees": 1e9}), json!(null)] { + assert!(s.execute("tools.rotateBrush", bad.clone()).is_err(), "{bad}"); + } + assert_eq!(s.tools.brush.angle, -15.0); + } + #[test] fn commands_step_the_session_brush_and_journal() { let mut s = Session::new(); diff --git a/crates/ui-egui/src/canvas_tool_menu.rs b/crates/ui-egui/src/canvas_tool_menu.rs index 13af25d845..b6b86111e1 100644 --- a/crates/ui-egui/src/canvas_tool_menu.rs +++ b/crates/ui-egui/src/canvas_tool_menu.rs @@ -123,6 +123,20 @@ const ENDS_TYPE_EDIT: [&str; 5] = /// The pasteboard menu's Select Custom Color… row. pub const PASTEBOARD_PICK: &str = "pasteboard.selectCustomColor"; +/// The pasteboard colours in the menu's order, which Space+F steps through. +const CANVAS_COLORS: [CanvasColor; 6] = + [CanvasColor::Default, CanvasColor::Black, CanvasColor::DarkGray, CanvasColor::MediumGray, CanvasColor::LightGray, CanvasColor::Custom]; + +/// Photoshop's Space+F (⇧Space+F backwards): the next pasteboard colour, round and round. Saved +/// like a choice in the pasteboard menu (`interface.canvasColor`). +pub fn cycle_canvas_color(app: &mut PhotocraftApp, backward: bool) -> Result<(), String> { + let n = CANVAS_COLORS.len(); + let at = CANVAS_COLORS.iter().position(|c| *c == app.session.prefs().interface.canvas_color).unwrap_or(0); + let next = if backward { (at + n - 1) % n } else { (at + 1) % n }; + let name = CANVAS_COLORS.get(next).map_or("default", |c| c.name()); + app.run("prefs.set", json!({"values": {"interface.canvasColor": name}})).map(|_| ()) +} + /// The Zoom and Hand tools' context menu: the zoom steps and fixed zooms of the View menu, plus /// the Hand tool's Fill Screen. pub const VIEW_MENU: &[Row] = &[ diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1ba0bce83d..d6309afccc 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2289,6 +2289,8 @@ mod color_replacement_tests; mod layer_shortcut_tests; #[cfg(test)] mod pencil_tests; +#[cfg(test)] +mod tool_shortcut_tests; #[cfg(test)] mod pixel_aspect_tests; diff --git a/crates/ui-egui/src/move_mods.rs b/crates/ui-egui/src/move_mods.rs index 193a70bb50..b3e7820dd1 100644 --- a/crates/ui-egui/src/move_mods.rs +++ b/crates/ui-egui/src/move_mods.rs @@ -175,7 +175,7 @@ pub fn arrow_keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool { use egui::{Key, Modifiers}; let cmd = ctx.input(|i| i.modifiers.command) && crate::hold_keys::cmd_nudges(app); if app.ui.tool != Tool::Move && app.ui.transform.is_none() && !cmd { - return selection_arrow_keys(app, ctx); + return selection_arrow_keys(app, ctx) || brush_arrow_keys(app, ctx); } // `consume_key` never ignores an extra ⌘, so it is part of every pattern while it moves. let base = if cmd { Modifiers::COMMAND } else { Modifiers::NONE }; @@ -193,6 +193,30 @@ pub fn arrow_keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool { false } +/// ← / → with a painting tool: turn the brush tip 1° (⇧ 15°), as Photoshop does; → turns it +/// clockwise. Returns true when a key was used. +fn brush_arrow_keys(app: &mut PhotocraftApp, ctx: &egui::Context) -> bool { + use egui::{Key, Modifiers}; + // Only plain or ⇧ arrows (`consume_key` ignores an extra ⌥, which kerns type), and never + // while type is being edited. + let m = ctx.input(|i| i.modifiers); + if !app.ui.tool.is_brushlike() || app.ui.text_edit.is_some() || m.alt || m.command || m.ctrl { + return false; + } + for (key, sign) in [(Key::ArrowLeft, 1.0), (Key::ArrowRight, -1.0)] { + // ⇧ first: `consume_key` ignores an extra ⇧. + for (mods, step) in [(Modifiers::SHIFT, 15.0), (Modifiers::NONE, 1.0)] { + if ctx.input_mut(|i| i.consume_key(mods, key)) { + if let Err(e) = app.run("tools.rotateBrush", json!({"degrees": sign * step})) { + app.ui.status = e; + } + return true; + } + } + } + false +} + /// Moves the selected layers (or the Free Transform box, or the selected pixels) by `(dx, dy)` /// pixels, as the Move tool (also when ⌘ makes it the temporary Move tool). pub fn nudge(app: &mut PhotocraftApp, dx: f64, dy: f64, duplicate_first: bool) { @@ -404,6 +428,27 @@ mod tests { arrow_keys(app, &ctx) } + /// Photoshop's ← / → with a painting tool turn the brush tip 1°, ⇧ 15°; → is clockwise. Other + /// tools keep their arrows (the Hand has none, the marquee nudges the selection). + #[test] + fn arrows_turn_the_brush_tip_for_painting_tools_only() { + let mut app = app_with_layer(); + for tool in [Tool::Brush, Tool::Eraser, Tool::CloneStamp] { + app.ui.tool = tool; + app.run("tools.setBrush", json!({"brush": {"angle": 0}})).unwrap(); + assert!(press(&mut app, egui::Key::ArrowLeft, egui::Modifiers::NONE), "{tool:?}"); + assert_eq!(app.session.tools.brush.angle, 1.0, "{tool:?}: ← anticlockwise"); + assert!(press(&mut app, egui::Key::ArrowRight, egui::Modifiers::SHIFT)); + assert_eq!(app.session.tools.brush.angle, -14.0, "{tool:?}: ⇧→ 15° clockwise"); + } + app.run("tools.setBrush", json!({"brush": {"angle": 0}})).unwrap(); + // ⌥-arrows are not the brush's (they kern type, and ⌘ moves). + assert!(!press(&mut app, egui::Key::ArrowLeft, egui::Modifiers::ALT)); + app.ui.tool = Tool::Hand; + assert!(!press(&mut app, egui::Key::ArrowLeft, egui::Modifiers::NONE)); + assert_eq!(app.session.tools.brush.angle, 0.0); + } + /// Real key events: ⇧ nudges 10 px and ⌥ duplicates first (egui ignores extra ⇧ / ⌥ when /// matching, so the plain arrow must not be tried first). #[test] @@ -433,8 +478,11 @@ mod tests { app.ui.tool = Tool::Brush; let id = app.session.active().unwrap().active_layer.unwrap(); let n0 = app.session.active().unwrap().doc.layers.len(); - assert!(!press(&mut app, egui::Key::ArrowRight, egui::Modifiers::NONE), "a plain arrow is not the Brush's"); + // A plain arrow turns the Brush's tip (below), never the layer. + assert!(press(&mut app, egui::Key::ArrowRight, egui::Modifiers::NONE)); assert_eq!(bounds(&app, id), photocraft_geom::Rect::new(8, 8, 24, 24)); + assert_eq!(app.session.tools.brush.angle, -1.0); + app.run("tools.setBrush", json!({"brush": {"angle": 0}})).unwrap(); for cmd in [CTRL, MAC_CMD] { assert!(press(&mut app, egui::Key::ArrowRight, cmd)); } diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 511265e1aa..7c0c8145f6 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -456,6 +456,17 @@ pub fn handle(app: &mut PhotocraftApp, ctx: &egui::Context) { return; } } + // Space+F (⇧Space+F backwards) cycles the pasteboard colour, as in Photoshop: F while the + // Hand key is held, before F's own Cycle Screen Mode. + if !matches!(focus, Focus::Text) && app.ui.text_edit.is_none() && crate::hold_keys::reposition_held(app, ctx) { + let back = ctx.input_mut(|i| i.consume_key(Modifiers::SHIFT, Key::F)); + if back || ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::F)) { + if let Err(e) = crate::canvas_tool_menu::cycle_canvas_color(app, back) { + app.ui.status = e; + } + return; + } + } // Inline type editing eats text and navigation keys; ⌘-shortcuts still reach the menus. let editing = crate::type_tool::handle_keys(app, ctx); // Registry, UI and menu-catalogue shortcuts (see [`crate::shortcut_dispatch::bindings`]). diff --git a/crates/ui-egui/src/tool_shortcut_tests.rs b/crates/ui-egui/src/tool_shortcut_tests.rs new file mode 100644 index 0000000000..712e7765a2 --- /dev/null +++ b/crates/ui-egui/src/tool_shortcut_tests.rs @@ -0,0 +1,39 @@ +//! Photoshop's tool keys that aren't plain shortcuts: Space+F cycles the pasteboard colour. + +use egui::{Key, Modifiers}; +use serde_json::json; + +use crate::PhotocraftApp; + +fn app() -> PhotocraftApp { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"width": 20, "height": 10})).unwrap(); + app +} + +/// Space+F steps the pasteboard colour forward and ⇧Space+F back, round and round; F alone is +/// still Cycle Screen Mode. +#[test] +fn space_f_cycles_the_canvas_colour() { + use photocraft_engine::prefs::CanvasColor; + let mut app = app(); + let ctx = egui::Context::default(); + let frame = |app: &mut PhotocraftApp, events: Vec| { + ctx.begin_pass(egui::RawInput { events, ..Default::default() }); + crate::shortcuts::handle(app, &ctx); + ctx.end_pass().textures_delta.clear(); + }; + let key = |key, pressed, modifiers| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }; + let color = |app: &PhotocraftApp| app.session.prefs().interface.canvas_color; + frame(&mut app, vec![key(Key::Space, true, Modifiers::NONE)]); + frame(&mut app, vec![key(Key::F, true, Modifiers::NONE), key(Key::F, false, Modifiers::NONE)]); + assert_eq!(color(&app), CanvasColor::Black); + frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::SHIFT), key(Key::F, true, Modifiers::SHIFT), key(Key::F, false, Modifiers::SHIFT)]); + frame(&mut app, vec![key(Key::F, true, Modifiers::SHIFT), key(Key::F, false, Modifiers::SHIFT)]); + assert_eq!(color(&app), CanvasColor::Custom, "backwards past Default wraps round"); + frame(&mut app, vec![egui::Event::ModifiersChanged(Modifiers::NONE), key(Key::Space, false, Modifiers::NONE)]); + let mode = app.ui.view.screen_mode.clone(); + frame(&mut app, vec![key(Key::F, true, Modifiers::NONE), key(Key::F, false, Modifiers::NONE)]); + assert_eq!(color(&app), CanvasColor::Custom, "F alone leaves the canvas colour"); + assert_ne!(app.ui.view.screen_mode, mode, "and cycles the screen mode"); +} diff --git a/crates/ui-egui/src/type_tool.rs b/crates/ui-egui/src/type_tool.rs index e1e3573e04..28d4d7c56b 100644 --- a/crates/ui-egui/src/type_tool.rs +++ b/crates/ui-egui/src/type_tool.rs @@ -251,8 +251,9 @@ pub fn pointer_down(app: &mut PhotocraftApp, x: f64, y: f64, mods: egui::Modifie commit_with(app, mods); return true; // Photoshop: a click outside commits without starting new text } - // The Type Mask tools always start new text: existing type layers are just pixels to them. - if app.ui.tool.is_type_mask() { + // The Type Mask tools always start new text: existing type layers are just pixels to them; + // so does a ⇧-click with the Type tool, as in Photoshop, to start new text over old. + if app.ui.tool.is_type_mask() || mods.shift { return false; } if let Some(id) = hit_layer(app, x, y) { @@ -2220,6 +2221,22 @@ mod tests { current_text(app, LayerId(ed.layer)).unwrap() } + /// Photoshop: a click on existing type edits it; a ⇧-click there starts a new type layer. + #[test] + fn shift_click_on_existing_type_starts_new_text() { + let mut app = app(); + pointer_up(&mut app, [50.0, 100.0], [50.0, 100.0]); + insert(&mut app, "Existing words"); + commit(&mut app); + let existing = app.session.active().unwrap().doc.layers.last().unwrap().id; + let (x, y) = (60.0, 95.0); + assert_eq!(hit_layer(&mut app, x, y), Some(existing), "the click lands on the existing text"); + assert!(!pointer_down(&mut app, x, y, egui::Modifiers::SHIFT), "⇧: not consumed, so a new type layer starts"); + assert!(app.ui.text_edit.is_none()); + assert!(pointer_down(&mut app, x, y, egui::Modifiers::NONE), "a plain click edits the existing text"); + assert_eq!(app.ui.text_edit.as_ref().map(|e| e.layer), Some(existing.0)); + } + #[test] fn click_creates_placeholder_selected_and_typing_replaces_it() { let mut app = app(); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3daee822c7..76b59de719 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity: tools, handles, modifiers and feel -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (⇧-dragged guides land on ruler ticks; layer and selection keys) · **Target:** Adobe Photoshop 2026 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (⇧-dragged guides land on ruler ticks; layer and selection keys; tool keys) · **Target:** Adobe Photoshop 2026 How PhotoCraft's direct manipulation compares with Photoshop 2026 (27.11.0): the toolbox, the handles and modifier keys of each gesture, nudging, snapping, numeric precision, navigation, @@ -184,6 +184,7 @@ Triangle and Curvature Pen (minus legacy Rounded Rectangle and Targeted Adjustme | Shortcut glyphs per platform | done (UI-217-19) | | | Brush size/hardness keys on non-US layouts | done (#3109): `[` `]` need AltGr on many layouts, so Photoshop uses other keys there (German: Ö / #, ⇧ for hardness). `layout_keys.rs` has a data table of such layouts (`LAYOUTS`, add rows to support one more) and a fallback for all others: the keys at the US `[` / `]` positions act as `[` / `]` when they don't type brackets. Off once a real bracket is typed; Edit › Keyboard Shortcuts overrides still apply | | | Layer and selection keys: ⌘, toggles visibility, ⌘\ targets the mask, / locks transparency, ⌘⌥2 loads luminosity, ⌘⌥⌫ fills from history, Tab / ⇧Tab rename the next layer, ⌥-click a thumbnail fits the layer | done (2026-10-11) | | +| Tool keys: ← / → turn the brush tip (1°, ⇧ 15°), ⇧-click with Type starts new text over old, Space+F / ⇧Space+F cycle the pasteboard colour | done (2026-10-11) | | ## Panels and workspace @@ -217,3 +218,4 @@ Paragraph; the rest open as in-app floating windows. | 2026-10-11 | minor | History snapshots: the camera button takes one, snapshot rows return to it as a new state, rename and delete it (#2966) | | 2026-10-11 | minor | ⇧-drag a guide out of a ruler lands on the nearest ruler tick (#3770) | | 2026-10-11 | minor | Layer and selection keys from Photoshop's shortcut set: ⌘, toggles, ⌘\, /, ⌘⌥2, ⌘⌥⌫, Tab rename-next, ⌥-click thumbnail to fit | +| 2026-10-11 | minor | Tool keys from Photoshop's shortcut set: ← / → rotate the brush tip, ⇧-click for new type, Space+F canvas colour | From 73ed08ddbe4ffbc0bc686e7c4acbb5aecbc81fb4 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:28:31 +0200 Subject: [PATCH 03/27] PSD type: read styles from the run's sheets and place text at its Txt2 frame (#3795, #1317) (#3819) * PSD type: resolve paragraph and character styles from the run's own sheets (#3795) apply_engine_data filled a run's missing attributes only from the Normal sheets (TheNormalParagraphSheet / TheNormalStyleSheet). A run that takes its alignment or size from a named sheet (by Parent index or Name), or from its paragraph's DefaultStyleSheet, was read with Normal's values, so a centred paragraph style came back Left and starting an edit re-laid the text half a line to the side. Resolve run -> named sheet -> paragraph's default character style -> Normal, ignoring missing or out-of-range references. On export, runs whose sheet reference is rewritten also get the attributes they inherited, so they keep their look whatever they now point to. * PSD type: place text at its Txt2 frame and keep box lines whose descent overhangs (#1317) Recent Photoshop keeps part of a type layer's position in its Txt2 frame: a frame-to-text-space matrix and, for point text, the anchor, with EngineData's PointBase left at the origin and BoxBounds in the frame's space. Import ignored them, so box text landed far off the canvas once an edit started (the text 'disappeared') and point text moved up a line and off its alignment anchor. Apply the frame when its origin lands in the TySh bounds (and not already without it, so a TySh PhotoCraft rewrote is not moved twice). Photoshop also keeps a box line whose baseline is inside the box while its descenders hang below it; layout dropped such lines (and some single-line boxes rendered nothing). --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/io/src/psd_import.rs | 1 + crates/io/tests/psd_style_sheets.rs | 225 ++++++++++++++++++++++++++++ crates/text/src/layout.rs | 6 +- crates/text/src/psd.rs | 203 +++++++++++++++++++++---- crates/text/src/psd_styles.rs | 27 +++- crates/text/src/tests.rs | 98 ++++++++++++ 6 files changed, 528 insertions(+), 32 deletions(-) create mode 100644 crates/io/tests/psd_style_sheets.rs diff --git a/crates/io/src/psd_import.rs b/crates/io/src/psd_import.rs index ffe2d34eaf..af19a0d76e 100644 --- a/crates/io/src/psd_import.rs +++ b/crates/io/src/psd_import.rs @@ -307,6 +307,7 @@ impl Ctx<'_> { }); if let Some(txt2) = &self.txt2 { photocraft_text::psd::apply_txt2(&mut t, &data, txt2); + photocraft_text::psd::apply_txt2_frame(&mut t, &data, txt2); } // Photoshop's pixels are the cache only when the file has some: ag-psd, GIMP and // other writers leave type layers without image data (Photoshop re-renders them on diff --git a/crates/io/tests/psd_style_sheets.rs b/crates/io/tests/psd_style_sheets.rs new file mode 100644 index 0000000000..0634dd3f9c --- /dev/null +++ b/crates/io/tests/psd_style_sheets.rs @@ -0,0 +1,225 @@ +//! #3795: a PSD type layer's alignment and character attributes can come from its style sheets +//! rather than its runs. A run's `Properties` / `StyleSheetData` hold only its overrides; the rest +//! comes from the paragraph style its `Name` names in `ParagraphSheetSet`, the character style in +//! `StyleSheetSet` (by name, or the paragraph's `DefaultStyleSheet`), then the Normal sheets. +//! Reading only the Normal sheets left a centred paragraph style read as Left, so starting an +//! edit re-laid the text out half a line to the side. + +use photocraft_doc::text::TextAlign; +use photocraft_geom::Affine; +use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value as D}; +use photocraft_text::engine_data::Value as E; +use photocraft_text::psd::{TySh, text_layer_from_tysh, write_tysh}; + +fn real(v: f64) -> E { + E::Real(v) +} + +fn d(kv: Vec<(&str, E)>) -> E { + E::Dict(kv.into_iter().map(|(k, v)| (k.to_string(), v)).collect()) +} + +/// Photoshop-shaped EngineData for one paragraph of point text. `para` is the paragraph run's +/// `ParagraphSheet`, `style` the character run's `StyleSheet`; the sheet sets are the +/// `ResourceDict`'s, with Normal at index 0. +fn engine(text: &str, para: E, style: E, para_set: Vec, style_set: Vec) -> E { + let ed_text = format!("{text}\r"); + let n = ed_text.encode_utf16().count() as i64; + let res = d(vec![ + ( + "FontSet", + E::Array(vec![d(vec![("Name", E::String("Inter-Regular".into())), ("Script", E::Int(0)), ("FontType", E::Int(1)), ("Synthetic", E::Int(0))])]), + ), + ("StyleSheetSet", E::Array(style_set)), + ("ParagraphSheetSet", E::Array(para_set)), + ("TheNormalStyleSheet", E::Int(0)), + ("TheNormalParagraphSheet", E::Int(0)), + ]); + d(vec![ + ( + "EngineDict", + d(vec![ + ("Editor", d(vec![("Text", E::String(ed_text))])), + ("ParagraphRun", d(vec![("RunArray", E::Array(vec![d(vec![("ParagraphSheet", para)])])), ("RunLengthArray", E::Array(vec![E::Int(n)]))])), + ("StyleRun", d(vec![("RunArray", E::Array(vec![d(vec![("StyleSheet", style)])])), ("RunLengthArray", E::Array(vec![E::Int(n)]))])), + ( + "Rendered", + d(vec![( + "Shapes", + d(vec![( + "Children", + E::Array(vec![d(vec![ + ("ShapeType", E::Int(0)), + ("Cookie", d(vec![("Photoshop", d(vec![("ShapeType", E::Int(0)), ("PointBase", E::Array(vec![real(0.0), real(0.0)]))]))])), + ])]), + )]), + )]), + ), + ]), + ), + ("ResourceDict", res), + ]) +} + +fn normal_char(size: f64) -> E { + d(vec![ + ("Name", E::String("Normal RGB".into())), + ("StyleSheetData", d(vec![("Font", E::Int(0)), ("FontSize", real(size)), ("AutoLeading", E::Bool(true))])), + ]) +} + +fn para_sheet(name: &str, justification: i64, default_style: i64) -> E { + d(vec![("Name", E::String(name.into())), ("DefaultStyleSheet", E::Int(default_style)), ("Properties", d(vec![("Justification", E::Int(justification))]))]) +} + +fn layer(text: &str, ed: &E) -> photocraft_doc::TextLayer { + let desc = Descriptor::new("TxLr") + .with("Txt ", D::Text(UnicodeString::new_nul(text))) + .with("Ornt", D::Enumerated { type_id: Id::new("Ornt"), value: Id::new("Hrzn") }) + .with("TextIndex", D::Integer(0)) + .with("EngineData", D::RawData(photocraft_text::engine_data::write(ed))); + let data = write_tysh(&TySh { transform: Affine::translate(450.0, 300.0), text: desc, warp: None, bounds: [0.0; 4] }); + let mut t = text_layer_from_tysh(&data, 72.0).expect("parses"); + // Import keeps the block, as for a layer read from a file. + t.psd_raw = Some(std::sync::Arc::new(data)); + t +} + +/// The paragraph run names a centred (or right-aligned) paragraph style and overrides nothing. +#[test] +fn alignment_from_the_paragraph_style_the_run_names() { + for (j, want) in [(2, TextAlign::Center), (1, TextAlign::Right)] { + let ed = engine( + "LOREM IPSUM", + d(vec![("Name", E::String("Title".into())), ("DefaultStyleSheet", E::Int(0)), ("Properties", d(vec![]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0), para_sheet("Title", j, 0)], + vec![normal_char(48.0)], + ); + let t = layer("LOREM IPSUM", &ed); + assert_eq!(t.paragraphs[0].style.align, want, "Justification {j} from the paragraph style"); + // Laid out around the anchor like Photoshop draws it. + let b = photocraft_text::TextEngine::new().layout(&t, 72.0).bounds().unwrap(); + match want { + TextAlign::Center => assert!((b[0] + b[2]).abs() < 1.0, "centred on the anchor: {b:?}"), + _ => assert!(b[2].abs() < 1.0, "ends at the anchor: {b:?}"), + } + } +} + +/// A run's own value still wins over its paragraph style, and an unknown style name or an +/// out-of-range sheet index falls back to Normal. +#[test] +fn run_overrides_win_and_bad_references_fall_back_to_normal() { + let ed = engine( + "Text", + d(vec![("Name", E::String("Title".into())), ("DefaultStyleSheet", E::Int(0)), ("Properties", d(vec![("Justification", E::Int(1))]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0), para_sheet("Title", 2, 0)], + vec![normal_char(48.0)], + ); + assert_eq!(layer("Text", &ed).paragraphs[0].style.align, TextAlign::Right); + let ed = engine( + "Text", + d(vec![("Name", E::String("Missing".into())), ("DefaultStyleSheet", E::Int(9)), ("Properties", d(vec![]))]), + d(vec![("Name", E::String("Missing".into())), ("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 2, 0)], + vec![normal_char(30.0)], + ); + let t = layer("Text", &ed); + assert_eq!(t.paragraphs[0].style.align, TextAlign::Center, "Normal's alignment"); + assert!((t.runs[0].style.size_pt - 30.0).abs() < 0.01, "Normal's size: {}", t.runs[0].style.size_pt); +} + +/// Character attributes from the paragraph's default character style (`DefaultStyleSheet`) and +/// from a character style the run names. +#[test] +fn character_attributes_from_the_style_sheets() { + let big = d(vec![("Name", E::String("Big".into())), ("StyleSheetData", d(vec![("FontSize", real(72.0))]))]); + let ed = engine( + "Text", + d(vec![("DefaultStyleSheet", E::Int(1)), ("Properties", d(vec![]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0)], + vec![normal_char(12.0), big.clone()], + ); + assert!((layer("Text", &ed).runs[0].style.size_pt - 72.0).abs() < 0.01, "the paragraph's default character style"); + let ed = engine( + "Text", + d(vec![("DefaultStyleSheet", E::Int(0)), ("Properties", d(vec![]))]), + d(vec![("Name", E::String("Big".into())), ("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0)], + vec![normal_char(12.0), big], + ); + let t = layer("Text", &ed); + assert!((t.runs[0].style.size_pt - 72.0).abs() < 0.01, "the named character style: {}", t.runs[0].style.size_pt); + // Other attributes still come from Normal. + assert_eq!( + t.runs[0].style.font_family, + layer( + "Text", + &engine( + "Text", + d(vec![("Properties", d(vec![]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0)], + vec![normal_char(12.0)] + ) + ) + .runs[0] + .style + .font_family + ); +} + +/// Export writes every run fully resolved and keeps the sheet sets: a round trip reads the same +/// alignment and size, with one entry per sheet. +#[test] +fn styles_from_sheets_survive_a_round_trip() { + use photocraft_color::{ColorMode, SampleType}; + use photocraft_doc::{Document, Layer, LayerContent, Size}; + let ed = engine( + "LOREM IPSUM", + d(vec![("Name", E::String("Title".into())), ("DefaultStyleSheet", E::Int(1)), ("Properties", d(vec![]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0), para_sheet("Title", 2, 1)], + vec![normal_char(12.0), d(vec![("Name", E::String("Big".into())), ("StyleSheetData", d(vec![("FontSize", real(48.0))]))])], + ); + let mut t = layer("LOREM IPSUM", &ed); + let mut doc = Document::new("t", Size::new(900, 600), ColorMode::Rgb, SampleType::U8); + doc.resolution_dpi = 72.0; + photocraft_text::TextEngine::new().render_layer(&mut t, 72.0, doc.pixel_format()); + doc.layers.push(Layer::new("t", LayerContent::Text(t))); + let bytes = photocraft_io::export(&doc, "t.psd", &Default::default()).unwrap().bytes; + let back = photocraft_io::import("t.psd", &bytes).unwrap().document; + let LayerContent::Text(bt) = &back.layers[0].content else { panic!("not a type layer") }; + assert_eq!(bt.paragraphs[0].style.align, TextAlign::Center); + assert!((bt.runs[0].style.size_pt - 48.0).abs() < 0.01, "{}", bt.runs[0].style.size_pt); +} + +/// A layer that inherits its alignment and size from sheets it no longer links to (a document +/// without those styles) keeps them through export: runs are written with what they inherited. +#[test] +fn unlinked_runs_keep_their_inherited_style_on_export() { + use photocraft_color::{ColorMode, SampleType}; + use photocraft_doc::{Document, Layer, LayerContent, Size}; + let ed = engine( + "LOREM IPSUM", + d(vec![("Name", E::String("Title".into())), ("DefaultStyleSheet", E::Int(1)), ("Properties", d(vec![]))]), + d(vec![("StyleSheetData", d(vec![]))]), + vec![para_sheet("Normal RGB", 0, 0), para_sheet("Title", 2, 1)], + vec![normal_char(12.0), d(vec![("Name", E::String("Big".into())), ("StyleSheetData", d(vec![("FontSize", real(48.0))]))])], + ); + let mut t = layer("LOREM IPSUM", &ed); + assert!(t.psd_raw.is_some()); + let mut doc = Document::new("t", Size::new(900, 600), ColorMode::Rgb, SampleType::U8); + doc.resolution_dpi = 72.0; + photocraft_text::TextEngine::new().render_layer(&mut t, 72.0, doc.pixel_format()); + doc.layers.push(Layer::new("t", LayerContent::Text(t))); + // No `text_styles_map::import`: the document has no styles and the runs link to none. + let bytes = photocraft_io::export(&doc, "t.psd", &Default::default()).unwrap().bytes; + let back = photocraft_io::import("t.psd", &bytes).unwrap().document; + let LayerContent::Text(bt) = &back.layers[0].content else { panic!("not a type layer") }; + assert_eq!(bt.paragraphs[0].style.align, TextAlign::Center); + assert!((bt.runs[0].style.size_pt - 48.0).abs() < 0.01, "{}", bt.runs[0].style.size_pt); +} diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index f7494bd992..7fdbb8a01f 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -702,7 +702,11 @@ impl Layouter { Some(b) => b + leading + pending_space, }; pending_space = 0.0; - let overflow = if vertical { baseline + descent > -box_rect.0 + 0.5 } else { baseline + descent > box_rect.1 + box_rect.3 + 0.5 }; + // Photoshop keeps a horizontal line whose baseline is inside the box and lets + // its descenders hang below the bottom edge (#1317: three 35 pt lines in a + // 91.3 pt box, the last baseline at 88). Requiring the descent to fit dropped + // that line. + let overflow = if vertical { baseline + descent > -box_rect.0 + 0.5 } else { baseline > box_rect.1 + box_rect.3 + 0.5 }; if is_box && overflow { stop = true; break; diff --git a/crates/text/src/psd.rs b/crates/text/src/psd.rs index b1359f31bb..27b1909510 100644 --- a/crates/text/src/psd.rs +++ b/crates/text/src/psd.rs @@ -12,7 +12,7 @@ use photocraft_color::{Color, ColorMode}; use photocraft_doc::TextLayer; use photocraft_doc::text::{AntiAlias, Caps, CharStyle, Kerning, Orientation, ParagraphRun, ParagraphStyle, TextAlign, TextRun, TextShape, TextWarp}; -use photocraft_geom::Affine; +use photocraft_geom::{Affine, Point}; use photocraft_psd::descriptor::{Descriptor, Id, UnicodeString, Value as D, VersionedDescriptor}; use crate::engine_data::{self as ed, Value as E}; @@ -212,6 +212,83 @@ pub fn apply_txt2(layer: &mut TextLayer, tysh: &[u8], txt2: &E) { layer.sync_summary(); } +/// The frame (`Txt2 /0 /8 /0` entry) of text object `index`: its rendered frame's `/10` names it. +fn txt2_frame(txt2: &E, index: usize) -> Option<&E> { + let object = txt2.path(&["1", "1"]).and_then(E::as_array)?.get(index)?; + let frame = + object.path(&["1", "2"]).and_then(E::as_array)?.first()?.get("6").and_then(E::as_array)?.iter().find_map(|f| f.get("10").and_then(E::as_i64))?; + txt2.path(&["0", "8", "0"]).and_then(E::as_array)?.get(usize::try_from(frame).ok()?)?.get("0") +} + +/// Places `layer` where Photoshop draws it when the text object's frame in `Txt2` is moved +/// (#1317). Recent Photoshop keeps a type layer's position partly in its `Txt2` frame: a matrix +/// (`/2 /2`) from the frame's space to the `TySh` text space and, for point text, the anchor +/// (`/0`), while EngineData's `PointBase` stays at the origin and `BoxBounds` stays in the +/// frame's space. Ignoring them put the text far from Photoshop's pixels (off the canvas, so +/// it "disappeared" as soon as an edit started), and point text a line too high and off its +/// alignment anchor. Only when the layer's `TextIndex` entry still holds the layer's text, as +/// for [`apply_txt2`], and the frame agrees with the `TySh` `bounds`. +pub fn apply_txt2_frame(layer: &mut TextLayer, tysh: &[u8], txt2: &E) { + let Some(t) = parse_tysh(tysh) else { return }; + let Some(index) = (match t.text.get("TextIndex") { + Some(D::Integer(i)) => usize::try_from(*i).ok(), + _ => None, + }) else { + return; + }; + let text = txt2.path(&["1", "1"]).and_then(E::as_array).and_then(|a| a.get(index)).and_then(|o| o.path(&["0", "0"])).and_then(E::as_str); + if text.is_none_or(|t| t.strip_suffix('\r').unwrap_or(t).replace('\r', "\n") != layer.text) { + return; + } + let Some(frame) = txt2_frame(txt2, index) else { return }; + let m = arr_f(frame.path(&["2", "2"])); + let matrix = if m.len() == 6 && m.iter().all(|v| v.is_finite()) { Affine { m: [m[0], m[1], m[2], m[3], m[4], m[5]] } } else { Affine::IDENTITY }; + let at = match layer.shape { + TextShape::Point => { + let p = arr_f(frame.get("0")); + if p.len() == 2 && p.iter().all(|v| v.is_finite()) { Affine::translate(p[0], p[1]) } else { Affine::IDENTITY } + } + TextShape::Box { .. } => Affine::IDENTITY, + }; + let to_text = matrix.mul(&at); + if to_text == Affine::IDENTITY || to_text.determinant().abs() <= 1e-9 { + return; + } + // The frame's origin (point text: the anchor; box text: the box's top left) must land in + // the text-space `bounds` Photoshop wrote beside it, and not already be there without it. A `TySh` PhotoCraft rewrote (an edited + // or moved layer whose text is unchanged keeps its old `Txt2` object and frame) already has + // the frame folded into its transform and its `bounds` near the origin: applying the frame + // again would move it twice. + let origin = match layer.shape { + TextShape::Point => Point::new(0.0, 0.0), + TextShape::Box { .. } => { + let b = engine_data(&t.text) + .map(|e| { + arr_f( + e.path(&["EngineDict", "Rendered", "Shapes", "Children"]) + .and_then(E::as_array) + .and_then(|c| c.first()) + .and_then(|s| s.path(&["Cookie", "Photoshop", "BoxBounds"])), + ) + }) + .unwrap_or_default(); + let [x, y, ..] = b[..] else { return }; + Point::new(x, y) + } + }; + let Some([l, top, r, bottom]) = descriptor_rect(&t.text, "bounds") else { return }; + let inside = |p: Point| p.x >= l - 1.0 && p.x <= r + 1.0 && p.y >= top - 1.0 && p.y <= bottom + 1.0; + if inside(origin) || !inside(to_text.apply(origin)) { + return; + } + { + // From the file's own transform: the frame's anchor is exact, so it replaces the + // ink-box estimate `legacy_point_origin` made for such point text (#1469), which put + // the baseline at the ink's top and the anchor at the ink's left whatever the alignment. + layer.transform = t.transform.mul(&to_text); + } +} + /// OpenType settings of a `Txt2` style that are on/off flags, by style key, as Photoshop /// writes them (each turned on alone in the Character panel's OpenType settings and read back): /// `/20` Contextual Alternates, `/23` Fractions, `/24` Ordinals, `/25` Swash, `/26` Titling @@ -380,46 +457,97 @@ fn apply_engine_data(layer: &mut TextLayer, e: &E, txt: Option<&str>, k: f32) { .and_then(E::as_array) .map(|a| a.iter().map(|f| f.get("Name").and_then(E::as_str).unwrap_or("").to_string()).collect()) .unwrap_or_default(); - let default_sheet = res.and_then(|r| r.get("TheNormalStyleSheet")).and_then(E::as_i64).unwrap_or(0) as usize; - let base_style = - res.and_then(|r| r.get("StyleSheetSet")).and_then(E::as_array).and_then(|a| a.get(default_sheet).or(a.first())).and_then(|s| s.get("StyleSheetData")); - let default_para_sheet = res.and_then(|r| r.get("TheNormalParagraphSheet")).and_then(E::as_i64).unwrap_or(0) as usize; - let base_para = res - .and_then(|r| r.get("ParagraphSheetSet")) - .and_then(E::as_array) - .and_then(|a| a.get(default_para_sheet).or(a.first())) - .and_then(|s| s.get("Properties")); + // Style resolution (#3795). A run's own `StyleSheetData` / `Properties` hold only what it + // overrides; the rest comes from its sheets, as in Photoshop: + // - a paragraph run: its `Properties` → the `ParagraphSheetSet` entry its `Name` names (a + // paragraph style; or its `Parent` index) → `TheNormalParagraphSheet`; + // - a character run: its `StyleSheetData` → the `StyleSheetSet` entry its `Parent` or `Name` + // names (a character style) → the default character style of its paragraph (the paragraph run's + // `DefaultStyleSheet`, else its paragraph style's) → `TheNormalStyleSheet`. + // Missing or out-of-range indices fall through to the next level, then to the defaults. + let style_set: &[E] = res.and_then(|r| r.get("StyleSheetSet")).and_then(E::as_array).unwrap_or(&[]); + let para_set: &[E] = res.and_then(|r| r.get("ParagraphSheetSet")).and_then(E::as_array).unwrap_or(&[]); + let index = |key: &str| res.and_then(|r| r.get(key)).and_then(E::as_i64).and_then(|i| usize::try_from(i).ok()); + let normal_style = index("TheNormalStyleSheet").and_then(|i| style_set.get(i)).or(style_set.first()); + let normal_para = index("TheNormalParagraphSheet").and_then(|i| para_set.get(i)).or(para_set.first()); + // A run names its sheet by a `Parent` index or by `Name` (see `psd_styles::reference`). + let named = |set: &'_ [E], sheet: &E| -> Option { + if let Some(i) = sheet.get("Parent").and_then(E::as_i64).and_then(|i| usize::try_from(i).ok()).filter(|i| *i < set.len()) { + return Some(i); + } + let name = sheet.get("Name").and_then(E::as_str)?; + set.iter().position(|s| s.get("Name").and_then(E::as_str) == Some(name)) + }; + + // Paragraph runs first: a character run's defaults depend on its paragraph. + let prun = e.path(&["EngineDict", "ParagraphRun"]); + let psheets: Vec<&E> = + prun.and_then(|r| r.get("RunArray")).and_then(E::as_array).map(|a| a.iter().filter_map(|x| x.get("ParagraphSheet")).collect()).unwrap_or_default(); + let plens16 = arr_f(prun.and_then(|r| r.get("RunLengthArray"))); + let plens = utf16_to_byte_lengths(&ed_text, &plens16); + let empty = E::Dict(Vec::new()); + // Per paragraph run: its merged base properties and its default character style. + let para_bases: Vec<(E, Option<&E>)> = psheets + .iter() + .map(|sheet| { + let style = named(para_set, sheet).and_then(|i| para_set.get(i)); + let base = merged(&[style.and_then(|s| s.get("Properties")), normal_para.and_then(|s| s.get("Properties"))]); + let char_default = [Some(*sheet), style] + .into_iter() + .flatten() + .find_map(|s| s.get("DefaultStyleSheet").and_then(E::as_i64).and_then(|i| usize::try_from(i).ok()).and_then(|i| style_set.get(i))); + (base, char_default) + }) + .collect(); + layer.paragraphs = psheets + .iter() + .zip(¶_bases) + .zip(plens) + .map(|((sheet, (base, _)), len)| ParagraphRun { len, style: para_style(Some(base), sheet.get("Properties").unwrap_or(&empty), k) }) + .collect(); // Character runs. let srun = e.path(&["EngineDict", "StyleRun"]); - let sdata: Vec<&E> = srun - .and_then(|r| r.get("RunArray")) - .and_then(E::as_array) - .map(|a| a.iter().filter_map(|x| x.path(&["StyleSheet", "StyleSheetData"])).collect()) - .unwrap_or_default(); - let slens = utf16_to_byte_lengths(&ed_text, &arr_f(srun.and_then(|r| r.get("RunLengthArray")))); + let ssheets: Vec<&E> = + srun.and_then(|r| r.get("RunArray")).and_then(E::as_array).map(|a| a.iter().filter_map(|x| x.get("StyleSheet")).collect()).unwrap_or_default(); + let slens16 = arr_f(srun.and_then(|r| r.get("RunLengthArray"))); + let slens = utf16_to_byte_lengths(&ed_text, &slens16); + // The paragraph run each character run starts in (UTF-16 offsets). + let para_of = |at: f64| -> Option { + let mut end = 0.0; + for (i, n) in plens16.iter().enumerate() { + end += n; + if at < end { + return Some(i); + } + } + plens16.len().checked_sub(1) + }; + let mut at16 = 0.0; + let bases: Vec = ssheets + .iter() + .enumerate() + .map(|(i, sheet)| { + let own = named(style_set, sheet).and_then(|i| style_set.get(i)); + let para_default = para_of(at16).and_then(|p| para_bases.get(p)).and_then(|(_, c)| *c); + at16 += slens16.get(i).copied().unwrap_or(0.0); + merged(&[own, para_default, normal_style].map(|s| s.and_then(|s| s.get("StyleSheetData")))) + }) + .collect(); + let sdata: Vec<&E> = ssheets.iter().map(|s| s.get("StyleSheetData").unwrap_or(&empty)).collect(); let incoming: Vec<(usize, bool, f32)> = sdata .iter() + .zip(&bases) .zip(&slens) - .map(|(d, len)| { - let auto = lookup(d, base_style, "AutoKerning").and_then(E::as_bool) != Some(false); - let kern = lookup(d, base_style, "Kerning").and_then(E::as_f64).filter(|v| v.is_finite()).unwrap_or(0.0) as f32; + .map(|((d, base), len)| { + let auto = lookup(d, Some(base), "AutoKerning").and_then(E::as_bool) != Some(false); + let kern = lookup(d, Some(base), "Kerning").and_then(E::as_f64).filter(|v| v.is_finite()).unwrap_or(0.0) as f32; (*len, auto, kern) }) .collect(); - layer.runs = sdata.iter().zip(slens).map(|(d, len)| TextRun { len, style: char_style(base_style, d, &fonts, k) }).collect(); + layer.runs = sdata.iter().zip(&bases).zip(slens).map(|((d, base), len)| TextRun { len, style: char_style(Some(base), d, &fonts, k) }).collect(); layer.runs = kerning_from_pairs(&layer.text, layer.char_runs(), &incoming); - // Paragraph runs. - let prun = e.path(&["EngineDict", "ParagraphRun"]); - let pdata: Vec<&E> = prun - .and_then(|r| r.get("RunArray")) - .and_then(E::as_array) - .map(|a| a.iter().filter_map(|x| x.path(&["ParagraphSheet", "Properties"])).collect()) - .unwrap_or_default(); - let plens = utf16_to_byte_lengths(&ed_text, &arr_f(prun.and_then(|r| r.get("RunLengthArray")))); - layer.paragraphs = pdata.iter().zip(plens).map(|(d, len)| ParagraphRun { len, style: para_style(base_para, d, k) }).collect(); - // Shape. let shape = e.path(&["EngineDict", "Rendered", "Shapes", "Children"]).and_then(E::as_array).and_then(|a| a.first()); if let Some(sh) = shape @@ -432,6 +560,21 @@ fn apply_engine_data(layer: &mut TextLayer, e: &E, txt: Option<&str>, k: f32) { } } +/// The dictionaries in `levels` as one: a key comes from the first level that has it. +fn merged(levels: &[Option<&E>]) -> E { + let mut out: Vec<(String, E)> = Vec::new(); + for level in levels.iter().flatten() { + if let E::Dict(kv) = level { + for (k, v) in kv { + if !out.iter().any(|(o, _)| o == k) { + out.push((k.clone(), v.clone())); + } + } + } + } + E::Dict(out) +} + fn lookup<'a>(run: &'a E, base: Option<&'a E>, key: &str) -> Option<&'a E> { run.get(key).or_else(|| base.and_then(|b| b.get(key))) } diff --git a/crates/text/src/psd_styles.rs b/crates/text/src/psd_styles.rs index fb3b297fdc..4caba32115 100644 --- a/crates/text/src/psd_styles.rs +++ b/crates/text/src/psd_styles.rs @@ -55,6 +55,20 @@ fn reference(sheet: Option<&E>, set_names: &[String], normal: usize) -> Option Option { let t = parse_tysh(tysh)?; @@ -170,6 +184,7 @@ pub fn write_style_sheets(tysh: &[u8], layer: &TextLayer, styles: &TextStyles, d rd.set("TheNormalParagraphSheet", E::Int(0)); rd.set("FontSet", E::Array(fonts.iter().map(|f| crate::psd::font_entry(f)).collect())); } + let char_fonts = e.path(&["ResourceDict", "FontSet"]).or_else(|| e.path(&["DocumentResources", "FontSet"])).map(|f| names(Some(f))).unwrap_or_default(); let char_ix = |id: Option| id.and_then(|id| styles.character.iter().position(|d| d.id == id)).map(|i| i as i64 + 1); let para_ix = |id: Option| id.and_then(|id| styles.paragraph.iter().position(|d| d.id == id)).map(|i| i as i64 + 1); let runs = layer.char_runs(); @@ -193,9 +208,16 @@ pub fn write_style_sheets(tysh: &[u8], layer: &TextLayer, styles: &TextStyles, d if let Some(sheet) = r.get_mut("StyleSheet") { remove_key(sheet, "Parent"); remove_key(sheet, "Name"); - if let Some(ix) = char_ix(run_at(start).and_then(|r| r.style.style_sheet)) { + let run = run_at(start); + if let Some(ix) = char_ix(run.and_then(|r| r.style.style_sheet)) { sheet.set("Parent", E::Int(ix)); } + // The run's reference may be dropped or renumbered: write what it inherited + // too, so it keeps its look whatever sheet it now points to (#3795). + if let Some(run) = run { + let fi = char_fonts.iter().position(|f| *f == postscript_for(&run.style)).unwrap_or(0); + fill_missing(sheet, "StyleSheetData", style_sheet_data(&run.style, fi, k)); + } } } } @@ -208,6 +230,9 @@ pub fn write_style_sheets(tysh: &[u8], layer: &TextLayer, styles: &TextStyles, d if let Some(ix) = para_ix(paras.get(i).and_then(|p| p.style.style_sheet)) { sheet.set("Parent", E::Int(ix)); } + if let Some(p) = paras.get(i) { + fill_missing(sheet, "Properties", paragraph_properties(&p.style, k)); + } } } } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index dc8df9e5e3..ae3092ca46 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1580,6 +1580,104 @@ fn a_served_script_fallback_joins_the_fallback_stack_once_it_arrives() { assert!(db.fallback_stack().any(|f| f == "Cairo"), "{:?}", db.fallback_stack().collect::>()); } +/// #1317: Photoshop keeps the last line of a text box whose baseline is inside the box even +/// when its descenders hang below the bottom edge (three 35 pt lines of 25 pt type in a +/// 91.3 pt box). A line whose baseline falls outside the box is still dropped. +#[test] +fn box_lines_whose_descent_hangs_below_the_box_are_kept() { + let mut e = TextEngine::new(); + let mut t = styled("one\ntwo\nthree", CharStyle { size_pt: 25.0, leading_pt: Some(35.0), ..Default::default() }); + t.shape = TextShape::Box { x: 0.0, y: 0.0, width: 300.0, height: 91.27 }; + let l = e.layout(&t, 72.0); + assert_eq!(l.lines.len(), 3, "{:?}", l.lines.iter().map(|l| l.baseline).collect::>()); + assert!(l.lines[2].baseline <= 91.27 + 0.5); + t.shape = TextShape::Box { x: 0.0, y: 0.0, width: 300.0, height: 80.0 }; + assert_eq!(e.layout(&t, 72.0).lines.len(), 2, "the third baseline (about 88) is below an 80 pt box"); +} + +/// `Txt2` with one text object for `text` whose rendered frame is frame 0, and that frame: +/// for point text an anchor `point`, and a frame-to-text matrix `matrix` (recent Photoshop, +/// #1317). +fn txt2_with_frame(text: &str, point: Option<[f64; 2]>, matrix: [f64; 6]) -> Vec { + let mut v = txt2_with_modes(text, &[(text.encode_utf16().count() + 1, 1)]); + // Close the object's model early: `… >> >> ] >>` → `… >> /1 << /2 [ frame ] >> >> ] >>`. + let tail = b"] >> >> >> ] >>"; + assert!(v.ends_with(tail)); + v.truncate(v.len() - tail.len()); + v.extend_from_slice(b"] >> >> /1 << /2 [ << /6 [ << /10 0 >> ] >> ] >> >> ] >>"); + let pt = point.map(|[x, y]| format!("/0 [ {x:.5} {y:.5} ] ")).unwrap_or_default(); + let m = matrix.map(|x| format!("{x:.5}")).join(" "); + let frames = format!("/0 << /8 << /0 [ << /0 << {pt}/2 << /2 [ {m} ] >> >> >> ] >> >>"); + let at = v.windows(9).position(|w| w == b"/0 << >> ").unwrap(); + v.splice(at..at + 9, frames.into_bytes().into_iter().chain(*b" ")); + v +} + +/// #1317: point text whose anchor Photoshop keeps only in its `Txt2` frame (EngineData's +/// `PointBase` at the origin, the transform far away) is placed at that anchor, not at the +/// ink's top left. A rewritten `TySh` (bounds near the origin) is left alone. +#[test] +fn txt2_frame_places_point_text_at_its_anchor() { + let mut t = point("2", 100.0); + t.transform = Affine::translate(-1000.0, -2000.0); + // Photoshop's text space: anchor at (1100, 2050), ink above its baseline. + let tysh = with_text_index(&crate::psd::build_tysh(&t, 72.0, Some([1100.0, 1980.0, 1160.0, 2050.0])), 0); + let txt2 = crate::psd::parse_txt2(&txt2_with_frame("2", Some([1000.0, 2100.0]), [1.0, 0.0, 0.0, 1.0, 100.0, -50.0])).unwrap(); + let mut l = crate::psd::text_layer_from_tysh(&tysh, 72.0).unwrap(); + crate::psd::apply_txt2_frame(&mut l, &tysh, &txt2); + let o = l.transform.apply(photocraft_geom::Point::new(0.0, 0.0)); + assert!((o.x - 100.0).abs() < 1e-3 && (o.y - 50.0).abs() < 1e-3, "anchor at {o:?}"); + // Already folded in (bounds near the origin): unchanged; another text: unchanged. + let tysh = with_text_index(&crate::psd::build_tysh(&t, 72.0, Some([0.0, -70.0, 60.0, 0.0])), 0); + let mut l = crate::psd::text_layer_from_tysh(&tysh, 72.0).unwrap(); + let before = l.transform; + crate::psd::apply_txt2_frame(&mut l, &tysh, &txt2); + assert_eq!(l.transform, before); + let tysh = with_text_index(&crate::psd::build_tysh(&t, 72.0, Some([1100.0, 1980.0, 1160.0, 2050.0])), 0); + let other = crate::psd::parse_txt2(&txt2_with_frame("3", Some([1000.0, 2100.0]), [1.0, 0.0, 0.0, 1.0, 100.0, -50.0])).unwrap(); + let mut l = crate::psd::text_layer_from_tysh(&tysh, 72.0).unwrap(); + let before = l.transform; + crate::psd::apply_txt2_frame(&mut l, &tysh, &other); + assert_eq!(l.transform, before); +} + +/// `tysh` with its text-space `bounds` set to `r` (Photoshop's, where it drew the text). +fn with_bounds(tysh: &[u8], r: [f64; 4]) -> Vec { + use photocraft_psd::descriptor::{Descriptor, Id, Value}; + let mut t = crate::psd::parse_tysh(tysh).unwrap(); + let mut d = Descriptor::default(); + for (k, v) in ["Left", "Top ", "Rght", "Btom"].iter().zip(r) { + d.items.push((Id::new(k), Value::Double(v))); + } + t.text.items.retain(|(k, _)| !k.is("bounds")); + t.text.items.push((Id::new("bounds"), Value::Descriptor(d))); + crate::psd::write_tysh(&t) +} + +/// #1317: a text box whose `BoxBounds` are in its `Txt2` frame's space is moved by the frame's +/// matrix onto the `bounds` Photoshop drew it at (it used to land off the canvas, so its text +/// "disappeared" when an edit started). +#[test] +fn txt2_frame_moves_box_text_onto_its_bounds() { + let mut t = styled("Casemiro 45", CharStyle { size_pt: 25.0, ..Default::default() }); + t.shape = TextShape::Box { x: 500.0, y: 600.0, width: 300.0, height: 90.0 }; + t.transform = Affine::translate(-700.0, -300.0); + let built = with_text_index(&crate::psd::build_tysh(&t, 72.0, None), 0); + let tysh = with_bounds(&built, [960.0, 300.0, 1260.0, 390.0]); + let txt2 = crate::psd::parse_txt2(&txt2_with_frame("Casemiro 45", None, [1.0, 0.0, 0.0, 1.0, 460.0, -300.0])).unwrap(); + let mut l = crate::psd::text_layer_from_tysh(&tysh, 72.0).unwrap(); + assert!(matches!(l.shape, TextShape::Box { .. })); + crate::psd::apply_txt2_frame(&mut l, &tysh, &txt2); + let TextShape::Box { x, y, .. } = l.shape else { panic!("not a box") }; + let o = l.transform.apply(photocraft_geom::Point::new(f64::from(x), f64::from(y))); + assert!((o.x - 260.0).abs() < 1e-3 && (o.y - 0.0).abs() < 1e-3, "box top left at {o:?}"); + // PhotoCraft's own `TySh` (bounds = the box, frame already folded in): unchanged. + let mut l = crate::psd::text_layer_from_tysh(&built, 72.0).unwrap(); + let before = l.transform; + crate::psd::apply_txt2_frame(&mut l, &built, &txt2); + assert_eq!(l.transform, before); +} + /// Preferences ▸ Type ▸ Missing Glyph Protection: off, only the default family is in the /// fallback stack, so a face without the glyph shows the missing-glyph box instead of a /// fallback's glyph (#1909). From 76fa06dbd5d5bf1cb40d6cf8f9d8b8c38eab6c5e Mon Sep 17 00:00:00 2001 From: Derek Gliwa Date: Mon, 12 Oct 2026 08:28:37 -0500 Subject: [PATCH 04/27] Merged and stamped layers keep their content bounds in CMYK and Lab (#3886) Merging or stamping converts the straight-RGBA composite into the document's colour model. Transparent black converts to K = 100% in CMYK and neutral a/b in Lab, so those pixels no longer equal the surface's default pixel, prune() keeps them, and the layer's content bounds span everything that was rendered: the whole canvas for Stamp Visible, Stamp Down and Copy Merged, and for Merge Layers, Merge Visible and Merge Down whenever an input may draw anywhere (a fill layer, even a masked one). Fully transparent pixels are now written as the default pixel in both conversion paths: photocraft_compose::flatten_to_surface (stamps, Copy Merged) and engine pixels::composite_layers (merges). Only formats with alpha are affected; over a Background everything stays opaque. Tests: engine merge_transparent_bounds (Merge Layers, Merge Visible over a transparent document, with a masked fill layer and onto a Background, Stamp Visible, Stamp Down, undo) in RGB/Gray at 8/16/32 bit and CMYK/Lab at 8/16 bit, and a compose unit test for flatten_to_surface. The masked fill layer test fails on main (merged layer = full canvas in CMYK). Co-authored-by: dgliwa <2328690+dgliwa@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/compose/src/lib.rs | 12 +- crates/compose/src/tests.rs | 28 ++++ .../engine/tests/merge_transparent_bounds.rs | 146 ++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 crates/engine/tests/merge_transparent_bounds.rs diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 08dcdeaf6a..cb88d002d9 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -233,6 +233,11 @@ pub fn band_rows_for(width: u32, requested: i32) -> i32 { /// The document's composite as a pixel surface in `fmt` (straight RGBA converted to its model /// and depth), optionally flattened over an opaque `background`; rendered and written in bands, /// so no full-size float composite is held. Tiles equal to the default pixel are pruned. +/// +/// Fully transparent pixels are written as the default pixel. Converting transparent black +/// leaves colour behind in CMYK (K = 100%) and Lab (neutral a/b = 0.5), and those pixels would +/// otherwise count as content: the result would span the whole canvas. +/// /// A CMYK document of plain Normal layers is composited in its ink values instead /// ([`native::flatten_cmyk`]), so flattening doesn't round-trip its pixels through RGB (#3778). pub fn flatten_to_surface(doc: &Document, fmt: photocraft_color::PixelFormat, background: Option<[f32; 3]>) -> Surface { @@ -249,6 +254,7 @@ pub fn composite_to_surface(doc: &Document, rect: Rect, fmt: photocraft_color::P } let mut s = Surface::new(fmt); let n = fmt.channels(); + let empty = s.default_pixel(); let mut data = Vec::new(); // The composite separates back to CMYK through the document's own profile, the one its // pixels were read through. @@ -262,7 +268,11 @@ pub fn composite_to_surface(doc: &Document, rect: Rect, fmt: photocraft_color::P data.resize(band.px.len() * n, 0.0); photocraft_color::convert::with_cmyk_space(cmyk.as_ref(), || { for (p, out) in band.px.iter().zip(data.chunks_exact_mut(n)) { - photocraft_raster::from_rgba_into(&fmt, *p, out); + if fmt.alpha && (p[3] <= 0.0 || p[3].is_nan()) && empty.len() == n { + out.copy_from_slice(&empty); + } else { + photocraft_raster::from_rgba_into(&fmt, *p, out); + } } }); s.write_region(band.rect, &data); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 869c316dd7..aca7a10c92 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -604,6 +604,34 @@ fn cmyk_document_renders_via_rgb() { assert!(p[0] < 0.3 && (p[0] - p[1]).abs() < 0.05 && (p[1] - p[2]).abs() < 0.05 && p[3] == 1.0, "{p:?}"); } +/// Transparent pixels convert to colour in CMYK (K = 100%) and Lab (a/b = 0.5). A flattened +/// surface must still treat them as empty, or its content spans the whole canvas. +#[test] +fn flatten_to_surface_leaves_transparent_pixels_empty_in_every_mode() { + let modes = [ + (ColorMode::Rgb, [SampleType::U8, SampleType::U16, SampleType::F32].as_slice()), + (ColorMode::Grayscale, &[SampleType::U8, SampleType::U16, SampleType::F32]), + (ColorMode::Cmyk, &[SampleType::U8, SampleType::U16]), + (ColorMode::Lab, &[SampleType::U8, SampleType::U16]), + ]; + for (mode, samples) in modes { + for &sample in samples { + let mut d = Document::new("t", Size::new(300, 200), mode, sample); + let fmt = PixelFormat::new(mode, sample, true); + let mut l = Layer::raster("dot", fmt); + let opaque: Vec = photocraft_raster::from_rgba(&fmt, [0.9, 0.4, 0.1, 1.0]); + l.surface_mut().unwrap().fill_rect(Rect::new(120, 80, 140, 90), &opaque); + d.layers.push(l); + let s = flatten_to_surface(&d, fmt, None); + assert_eq!(s.content_bounds(), Rect::new(120, 80, 140, 90), "{mode:?} {sample:?}"); + // Over a background everything is content, as before. + let fmt_opaque = PixelFormat::new(mode, sample, false); + let s = flatten_to_surface(&d, fmt_opaque, Some([1.0, 1.0, 1.0])); + assert_eq!(s.rgba(5, 5)[3], 1.0, "{mode:?} {sample:?}: background stays opaque"); + } + } +} + #[test] fn sixteen_bit_and_float_layers_composite() { for fmt in [PixelFormat::RGBA16, PixelFormat::RGBA32F] { diff --git a/crates/engine/tests/merge_transparent_bounds.rs b/crates/engine/tests/merge_transparent_bounds.rs new file mode 100644 index 0000000000..c796007233 --- /dev/null +++ b/crates/engine/tests/merge_transparent_bounds.rs @@ -0,0 +1,146 @@ +//! Merging small layers gives a layer as big as their content in every colour mode. In CMYK and +//! Lab, transparent pixels used to keep the colour of converted transparent black, so Merge +//! Layers, Merge Visible, Stamp Visible and Stamp Down made layers that spanned the whole canvas: +//! Free Transform, the Move tool and alignment then worked on the canvas instead of the content. + +use photocraft_doc::{Layer, LayerId}; +use photocraft_engine::Session; +use photocraft_engine::transform_cmds::transform_bounds; +use photocraft_geom::Rect; +use serde_json::{Value, json}; + +const W: u32 = 300; +const H: u32 = 400; +/// Two small shapes far from the canvas edges, and the box around both. +const A: [i32; 4] = [40, 300, 20, 30]; +const B: [i32; 4] = [90, 320, 25, 20]; +const BOTH: Rect = Rect { x0: 40, y0: 300, x1: 115, y1: 340 }; + +/// Every mode at the depths it supports (Photoshop has no 32-bit CMYK or Lab). +const CASES: &[(&str, u32)] = + &[("rgb", 8), ("rgb", 16), ("rgb", 32), ("gray", 8), ("gray", 16), ("gray", 32), ("cmyk", 8), ("cmyk", 16), ("lab", 8), ("lab", 16)]; + +fn id_of(r: &Value) -> LayerId { + LayerId(r["layer"].as_u64().unwrap()) +} + +/// A transparent document with the two shapes on their own layers. +fn scene(mode: &str, depth: u32) -> (Session, LayerId, LayerId) { + let mut s = Session::new(); + s.execute("file.new", json!({"width": W, "height": H, "mode": mode, "depth": depth, "background": "transparent"})).unwrap(); + let a = id_of(&s.execute("shape.create", json!({"kind": "rect", "rect": A, "fill": "#e07020"})).unwrap()); + let b = id_of(&s.execute("shape.create", json!({"kind": "rect", "rect": B, "fill": "#3060c0"})).unwrap()); + (s, a, b) +} + +fn select(s: &mut Session, ids: &[LayerId]) { + s.execute("layer.select", json!({"layer": ids[0].0})).unwrap(); + for id in &ids[1..] { + s.execute("layer.select", json!({"layer": id.0, "mode": "add"})).unwrap(); + } +} + +fn active(s: &Session) -> &Layer { + let st = s.active().unwrap(); + st.doc.layer(st.active_layer.unwrap()).unwrap() +} + +/// The merged layer hugs the shapes (anti-aliased edges may add a pixel) and still shows them. +fn assert_tight(s: &Session, what: &str) { + let doc = &s.active().unwrap().doc; + let layer = active(s); + let b = transform_bounds(doc, layer); + let grown = Rect::new(BOTH.x0 - 1, BOTH.y0 - 1, BOTH.x1 + 1, BOTH.y1 + 1); + assert!(grown.contains_rect(&b) && b.contains_rect(&Rect::new(BOTH.x0 + 1, BOTH.y0 + 1, BOTH.x1 - 1, BOTH.y1 - 1)), "{what}: bounds {b:?}, want ~{BOTH:?}"); + let surf = layer.surface().unwrap(); + // Inside each shape: opaque. Between them and far away: transparent. + assert!(surf.rgba(A[0] + 5, A[1] + 5)[3] > 0.99, "{what}: first shape kept"); + assert!(surf.rgba(B[0] + 5, B[1] + 5)[3] > 0.99, "{what}: second shape kept"); + assert!(surf.rgba(70, 310)[3] < 0.01, "{what}: gap stays transparent"); + assert!(surf.rgba(5, 5)[3] < 0.01, "{what}: corner stays transparent"); +} + +#[test] +fn merge_layers_keeps_content_bounds_in_every_mode() { + for &(mode, depth) in CASES { + let (mut s, a, b) = scene(mode, depth); + select(&mut s, &[a, b]); + s.execute("layer.mergeLayers", json!({})).unwrap(); + assert_tight(&s, &format!("Merge Layers {mode} {depth}")); + } +} + +#[test] +fn merge_visible_keeps_content_bounds_in_every_mode() { + for &(mode, depth) in CASES { + let (mut s, _, _) = scene(mode, depth); + s.execute("layer.mergeVisible", json!({})).unwrap(); + assert_tight(&s, &format!("Merge Visible {mode} {depth}")); + } +} + +#[test] +fn stamps_keep_content_bounds_in_every_mode() { + for &(mode, depth) in CASES { + let (mut s, _, _) = scene(mode, depth); + s.execute("layer.stampVisible", json!({})).unwrap(); + assert_tight(&s, &format!("Stamp Visible {mode} {depth}")); + + let (mut s, a, b) = scene(mode, depth); + select(&mut s, &[a, b]); + s.execute("layer.stampDown", json!({})).unwrap(); + assert_tight(&s, &format!("Stamp Down {mode} {depth}")); + } +} + +/// A fill layer may draw anywhere, so merging one renders the whole canvas; its mask still +/// limits the merged layer to the shapes. +#[test] +fn merging_a_masked_fill_layer_keeps_content_bounds_in_every_mode() { + let masked_scene = |mode: &str, depth: u32| { + let (mut s, a, _) = scene(mode, depth); + s.execute("select.rect", json!({"x": B[0], "y": B[1], "width": B[2], "height": B[3]})).unwrap(); + let fill = id_of(&s.execute("layer.newFillLayer.solidColor", json!({"color": "#3060c0"})).unwrap()); + s.execute("select.deselect", json!({})).unwrap(); + (s, a, fill) + }; + for &(mode, depth) in CASES { + let (mut s, a, fill) = masked_scene(mode, depth); + select(&mut s, &[a, fill]); + s.execute("layer.mergeLayers", json!({})).unwrap(); + assert_tight(&s, &format!("Merge Layers with a fill layer {mode} {depth}")); + + let (mut s, _, _) = masked_scene(mode, depth); + s.execute("layer.mergeVisible", json!({})).unwrap(); + assert_tight(&s, &format!("Merge Visible with a fill layer {mode} {depth}")); + } +} + +/// Over an opaque Background, Merge Visible still makes an opaque, canvas-sized Background. +#[test] +fn merge_visible_onto_a_background_stays_opaque() { + for &(mode, depth) in CASES { + let mut s = Session::new(); + s.execute("file.new", json!({"width": W, "height": H, "mode": mode, "depth": depth, "background": "white"})).unwrap(); + s.execute("shape.create", json!({"kind": "rect", "rect": A, "fill": "#e07020"})).unwrap(); + s.execute("layer.mergeVisible", json!({})).unwrap(); + let what = format!("{mode} {depth}"); + let doc = &s.active().unwrap().doc; + let layer = active(&s); + assert_eq!(layer.name, "Background", "{what}"); + assert_eq!(transform_bounds(doc, layer), Rect::new(0, 0, W as i32, H as i32), "{what}"); + let white = layer.surface().unwrap().rgba(5, 5); + assert!(white.iter().all(|c| *c > 0.98), "{what}: Background stays white, got {white:?}"); + } +} + +/// Undo restores both layers after the fixed merge. +#[test] +fn merge_layers_in_cmyk_is_one_undo_step() { + let (mut s, a, b) = scene("cmyk", 8); + select(&mut s, &[a, b]); + s.execute("layer.mergeLayers", json!({})).unwrap(); + s.execute("edit.undo", json!({})).unwrap(); + let doc = &s.active().unwrap().doc; + assert!(doc.layer(a).is_some() && doc.layer(b).is_some()); +} From 4010630c98084b9e296dbc845c477a0bfeb662ef Mon Sep 17 00:00:00 2001 From: jonathanwxh-cell Date: Mon, 12 Oct 2026 21:32:37 +0800 Subject: [PATCH 05/27] Preserve surviving channel targets and visibility on deletion (#3887) Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com> Co-authored-by: Codex --- crates/engine/src/channel_cmds.rs | 4 ++ crates/engine/src/channel_cmds/tests.rs | 50 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/crates/engine/src/channel_cmds.rs b/crates/engine/src/channel_cmds.rs index 39ce9c8250..63f8f5b626 100644 --- a/crates/engine/src/channel_cmds.rs +++ b/crates/engine/src/channel_cmds.rs @@ -1126,11 +1126,15 @@ fn delete_channel(s: &mut Session, p: &Value) -> Result { _ => return Err(bad(cmd, "missing `channel`")), }, }; + // `edit` validates the view against the shorter channel list before we can remap it. + // Keep the original indices and eyes so a surviving final channel is not discarded. + let view = s.active().ok_or(EngineError::NoDocument)?.channel_view.clone(); s.edit("Delete Channel", |doc, _| { doc.channels.remove(i); Ok(()) })?; if let Some(st) = s.active_mut() { + st.channel_view = view; let v = &mut st.channel_view; if i < v.alpha_visible.len() { v.alpha_visible.remove(i); diff --git a/crates/engine/src/channel_cmds/tests.rs b/crates/engine/src/channel_cmds/tests.rs index 17727dc8ad..d39dda70b7 100644 --- a/crates/engine/src/channel_cmds/tests.rs +++ b/crates/engine/src/channel_cmds/tests.rs @@ -168,6 +168,56 @@ fn channel_management_and_view_state() { assert_eq!(doc(&s).channels.len(), 3); } +#[test] +fn deleting_an_earlier_channel_keeps_the_last_channel_targeted() { + for depth in [8, 16, 32] { + let mut s = session_depth(depth); + s.execute("channel.new", json!({"name": "Remove", "fill": "black"})).unwrap(); + s.execute("channel.new", json!({"name": "Keep", "fill": "white"})).unwrap(); + let layer_before = px(&s, 5, 5); + s.execute("channel.delete", json!({"channel": "Remove"})).unwrap(); + assert_eq!(doc(&s).channels[0].name, "Keep"); + let view = &s.active().unwrap().channel_view; + assert_eq!(view.target, ChannelTarget::Alpha(0), "the surviving target follows its new index"); + assert!(view.alpha_shown(0)); + assert_eq!(view.visible_colors(3), 0, "keep the channel-only view"); + + s.execute("image.adjustments.invert", json!({})).unwrap(); + assert_eq!(chan(&s, 0, 5, 5), 0.0, "implicit pixel edits still target Keep"); + assert_eq!(px(&s, 5, 5), layer_before, "the layer must not be edited instead"); + assert!(s.undo()); + assert_eq!(chan(&s, 0, 5, 5), 1.0); + assert_eq!(px(&s, 5, 5), layer_before); + assert!(s.redo()); + assert_eq!(chan(&s, 0, 5, 5), 0.0); + assert_eq!(px(&s, 5, 5), layer_before); + } +} + +#[test] +fn deleting_a_channel_remaps_all_surviving_eyes_and_targets() { + for target_index in 0..3 { + let mut s = session(); + for name in ["First", "Middle", "Last"] { + s.execute("channel.new", json!({"name": name})).unwrap(); + } + s.execute("channel.target", json!({"channel": target_index})).unwrap(); + for (channel, visible) in [false, false, true].into_iter().enumerate() { + s.execute("channel.setVisible", json!({"channel": channel, "visible": visible})).unwrap(); + } + s.execute("channel.delete", json!({"channel": "Middle"})).unwrap(); + let view = &s.active().unwrap().channel_view; + let expected = match target_index { + 0 => ChannelTarget::Alpha(0), + 1 => ChannelTarget::Composite, + _ => ChannelTarget::Alpha(1), + }; + assert_eq!(view.target, expected, "old target {target_index}"); + assert!(!view.alpha_shown(0)); + assert!(view.alpha_shown(1), "Last keeps its eye when Middle is removed"); + } +} + #[test] fn every_colour_channel_can_hide_while_alpha_stays() { let mut s = session(); From 7958dc63fb8143bdb208d96d7d641f392d65e321 Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:42:58 +0200 Subject: [PATCH 06/27] Layers: Merge Group for a selected group (#3832) (#3888) * Layers: Merge Group for a selected group (#3832) With a lone group selected, Layer > Merge Layers (Cmd+E) reads Merge Group and the Layers panel's right-click menu offers Merge Group instead of Merge Down, as in Photoshop. The new layer.mergeGroup command flattens the group's visible contents into one pixel layer in its place, named after the group, keeping its opacity, blend mode (Pass Through becomes Normal), clipping and locks. Before, Cmd+E on a group ran Merge Down and merged the whole group into the layer below it. * i18n: Vietnamese row for Merge Group Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/engine/src/commands.rs | 38 +++++++++++++++++++++++++++ crates/engine/src/layer_multi_cmds.rs | 6 +++++ crates/engine/src/tests.rs | 38 +++++++++++++++++++++++++++ crates/ui-egui/src/i18n/cs.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/el.tsv | 1 + crates/ui-egui/src/i18n/es.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/id.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/ko.tsv | 1 + crates/ui-egui/src/i18n/nl.tsv | 1 + crates/ui-egui/src/i18n/pl.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/vi.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/i18n/zh-hant.tsv | 1 + crates/ui-egui/src/layer_menu_ui.rs | 28 ++++++++++++++++++++ crates/ui-egui/src/menus.rs | 8 ++++++ 22 files changed, 135 insertions(+) diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index a47bde9fff..5524388c71 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -782,6 +782,13 @@ fn build() -> Vec { })?; Ok(Value::Null) }), + cmd!("layer.mergeGroup", "Merge Group", ["Layer"], None, r##"{"layer":id?}"##, has_layer, |s, p| { + let id = layer_param(s, p)?; + if !s.active().and_then(|d| d.doc.layer(id)).is_some_and(|l| matches!(l.content, LayerContent::Group(_))) { + return Err(EngineError::Other("Merge Group needs a group".into())); + } + merge_group(s, id) + }), cmd!("layer.flattenImage", "Flatten Image", ["Layer"], None, "{}", has_doc, |s, _| { s.edit("Flatten Image", |doc, active| { let fmt = doc.pixel_format(); @@ -1427,6 +1434,37 @@ pub(crate) fn layer_copy(doc: &Document, id: LayerId) -> Result { Ok(dup) } +/// Layer › Merge Group (⌘E with a group selected, #3832): the group's visible contents, as they +/// show, become one pixel layer in its place, named after it. Like Photoshop, the layer keeps the +/// group's opacity, blend mode (Pass Through becomes Normal), clipping and locks; the group's mask +/// and style are applied to the pixels. +fn merge_group(s: &mut Session, id: LayerId) -> Result { + s.edit("Merge Group", |doc, active| { + let group = doc.layer(id).ok_or(EngineError::NoLayer(id))?.clone(); + if !group.visible { + return Err(EngineError::Other("the group is hidden".into())); + } + let fmt = doc.pixel_format(); + let fmt = photocraft_color::PixelFormat::new(fmt.mode, fmt.sample, true); + let mut solo = group.clone(); + solo.opacity = 1.0; + solo.blend = photocraft_doc::BlendMode::Normal; + solo.clipped = false; + let mut tmp = doc.clone(); + tmp.layers = vec![solo]; + let mut merged = Layer::new(group.name.clone(), LayerContent::Raster(pixels::composite_layers(&tmp, fmt, None))); + merged.id = group.id; + merged.opacity = group.opacity; + merged.blend = if group.blend == photocraft_doc::BlendMode::PassThrough { photocraft_doc::BlendMode::Normal } else { group.blend }; + merged.clipped = group.clipped; + merged.locks = group.locks; + *doc.layer_mut(id).ok_or(EngineError::NoLayer(id))? = merged; + *active = Some(id); + Ok(()) + })?; + Ok(json!({ "layer": id.0 })) +} + #[cfg(test)] mod registry_tests { use super::*; diff --git a/crates/engine/src/layer_multi_cmds.rs b/crates/engine/src/layer_multi_cmds.rs index 2d51dfdf0c..db96c3c1b4 100644 --- a/crates/engine/src/layer_multi_cmds.rs +++ b/crates/engine/src/layer_multi_cmds.rs @@ -1217,6 +1217,12 @@ pub fn group_layers(s: &mut Session, p: &Value) -> Result { fn merge_layers(s: &mut Session) -> Result { let sel = selected(s); if sel.len() < 2 { + // ⌘E on a lone group is Photoshop's Merge Group (#3832); on a layer, Merge Down. + if let Some(&id) = sel.first() + && s.active().and_then(|d| d.doc.layer(id)).is_some_and(|l| matches!(l.content, LayerContent::Group(_))) + { + return s.execute("layer.mergeGroup", json!({ "layer": id.0 })); + } return s.execute("layer.mergeDown", json!({})); } let mid = s.edit("Merge Layers", |doc, active| { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index a35a19bb43..08e17bd39c 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1469,3 +1469,41 @@ fn delete_layer_preserves_an_unaffected_active_layer_and_last_layer_guard() { assert_eq!(s.active().unwrap().active_layer, Some(other)); assert_eq!(s.active().unwrap().history.past_len(), before); } + +#[test] +fn merge_down_on_a_group_merges_the_group() { + // #3832: with a group selected, Merge Down is Photoshop's Merge Group (⌘E): the group's + // contents become one layer in its place, named after it, and the layer below is untouched. + let mut s = session_with_doc(); + let below = s.execute("layer.new.layer", json!({"name": "Below"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("edit.fill", json!({"color": "#00ff00"})).unwrap(); + let a = s.execute("layer.new.layer", json!({"name": "A"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("select.rect", json!({"x": 0, "y": 0, "width": 10, "height": 48})).unwrap(); + s.execute("edit.fill", json!({"color": "#ff0000"})).unwrap(); + s.execute("select.deselect", json!({})).unwrap(); + let b = s.execute("layer.new.layer", json!({"name": "B"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("select.rect", json!({"x": 20, "y": 0, "width": 10, "height": 48})).unwrap(); + s.execute("edit.fill", json!({"color": "#0000ff"})).unwrap(); + s.execute("select.deselect", json!({})).unwrap(); + s.execute("layer.select", json!({"layer": b})).unwrap(); + s.execute("layer.select", json!({"layer": a, "mode": "add"})).unwrap(); + let group = s.execute("layer.groupLayers", json!({"name": "Badge"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("layer.setProps", json!({"layer": group, "opacity": 0.5})).ok(); + let before = [px(&mut s, 5, 5), px(&mut s, 25, 5), px(&mut s, 50, 5)]; + let layers = s.active().unwrap().doc.layer_count(); + s.execute("layer.select", json!({"layer": group})).unwrap(); + s.execute("layer.mergeLayers", json!({})).unwrap(); + let st = s.active().unwrap(); + let merged = st.doc.layer(st.active_layer.unwrap()).unwrap().clone(); + assert_eq!(merged.name, "Badge"); + assert!(matches!(merged.content, photocraft_doc::LayerContent::Raster(_)), "one pixel layer"); + assert!(st.doc.layer(photocraft_doc::LayerId(below)).is_some(), "the layer below stays"); + assert_eq!(st.doc.layer_count(), layers - 2, "group and its two layers became one"); + let after = [px(&mut s, 5, 5), px(&mut s, 25, 5), px(&mut s, 50, 5)]; + for (a, b) in before.iter().zip(&after) { + assert!(a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1.0 / 255.0), "{before:?} -> {after:?}"); + } + // Undo brings the group back. + s.execute("edit.undo", json!({})).unwrap(); + assert!(s.active().unwrap().doc.layer(photocraft_doc::LayerId(group)).is_some()); +} diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 0b74504d51..e7de3409af 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -398,6 +398,7 @@ Select Linked Layers Vybrat propojené vrstvy Merge Layers Sloučit vrstvy Merge Down Sloučit dolů + Merge Group Sloučit skupinu Merge Visible Sloučit viditelné Stamp Visible Otisknout viditelné Select Layer Above Vybrat vrstvu nad diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 604dcb43f0..ac313154db 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -394,6 +394,7 @@ Select Linked Layers Verknüpfte Ebenen auswählen Merge Layers Ebenen zusammenfügen Merge Down Nach unten zusammenfügen + Merge Group Gruppe zusammenfügen Merge Visible Sichtbare zusammenfügen Stamp Visible Sichtbare Ebenen stempeln Select Layer Above Ebene darüber auswählen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 17677731dd..e5be105597 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1838,6 +1838,7 @@ New Guide Layout… Νέα διάταξη οδηγών… New Guides From Shape Νέοι οδηγοί από σχήμα Merge Down Συγχώνευση με το υποκείμενο επίπεδο + Merge Group Συγχώνευση ομάδας Stamp Visible Συνένωση ορατών σε νέο επίπεδο Select Layer Above Επιλογή επάνω επιπέδου Select Layer Below Επιλογή κάτω επιπέδου diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7ad6d980e8..b986956544 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -391,6 +391,7 @@ Select Linked Layers Seleccionar capas vinculadas Merge Layers Combinar capas Merge Down Combinar hacia abajo + Merge Group Combinar grupo Merge Visible Combinar visibles Stamp Visible Sellar visibles Select Layer Above Seleccionar la capa de arriba diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 7863936bfc..f426b28c56 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -387,6 +387,7 @@ Select Linked Layers Sélectionner les calques liés Merge Layers Fusionner les calques Merge Down Fusionner avec le calque inférieur + Merge Group Fusionner le groupe Merge Visible Fusionner les calques visibles Stamp Visible Tamponner les calques visibles Stamp Down Tamponner sur le calque inférieur diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 74bd4462f5..420600cf3f 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -380,6 +380,7 @@ Select Linked Layers Pilih Layer Tertaut Merge Layers Gabungkan Layer Merge Down Gabung ke Bawah + Merge Group Gabungkan Grup Merge Visible Gabung yang Terlihat Stamp Visible Stamp yang Terlihat Stamp Down Stamp ke Bawah diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 3d2a0efb34..410ab8f609 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -390,6 +390,7 @@ Select Linked Layers Seleziona livelli collegati Merge Layers Unisci livelli Merge Down Unisci sotto + Merge Group Unisci gruppo Merge Visible Unisci visibili Stamp Visible Unisci visibili in nuovo Select Layer Above Seleziona livello sopra diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 2196441a31..7e5e65a639 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -391,6 +391,7 @@ Select Linked Layers リンクしたレイヤーを選択 Merge Layers レイヤーを結合 Merge Down 下のレイヤーと結合 + Merge Group グループを結合 Merge Visible 表示レイヤーを結合 Stamp Visible 表示レイヤーをスタンプ Select Layer Above 上のレイヤーを選択 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 9476f0dab4..f48633c2cb 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -375,6 +375,7 @@ Select Linked Layers 연결된 레이어 선택 Merge Layers 레이어 병합 Merge Down 아래 레이어와 병합 + Merge Group 그룹 병합 Merge Visible 보이는 레이어 병합 Stamp Visible 보이는 레이어의 병합 사본 만들기 Select Layer Above 위 레이어 선택 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index fda481dc97..3778a8a1de 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -383,6 +383,7 @@ Select Linked Layers Gekoppelde lagen selecteren Merge Layers Lagen samenvoegen Merge Down Omlaag samenvoegen + Merge Group Groep samenvoegen Merge Visible Zichtbare samenvoegen Stamp Visible Zichtbare stempelen Select Layer Above Laag erboven selecteren diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ed63d43905..ce9387b623 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -389,6 +389,7 @@ Select Linked Layers Zaznacz połączone warstwy Merge Layers Scal warstwy Merge Down Scal w dół + Merge Group Scal grupę Merge Visible Scal widoczne Stamp Visible Odbij widoczne Select Layer Above Zaznacz warstwę powyżej diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a4242dc8c8..92e9c238b1 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -383,6 +383,7 @@ Select Linked Layers Selecionar camadas vinculadas Merge Layers Mesclar camadas Merge Down Mesclar para baixo + Merge Group Mesclar grupo Merge Visible Mesclar visíveis Stamp Visible Carimbar visíveis Stamp Down Carimbar para baixo diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ead84742f9..7e1d12bbb1 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1897,6 +1897,7 @@ Lock Layers… Заблокировать слои… Mean Среднее Merge Down Выполнить сведение + Merge Group Объединить группу Merge Layers Объединить слои Merge Shape Components Объединить компоненты фигуры Merge Visible Свести видимые diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7f59884e80..11a4caa9cf 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1876,6 +1876,7 @@ Lock Layers… Заблокувати шари… Mean Середнє значення Merge Down Об'єднати з нижнім + Merge Group Об'єднати групу Merge Layers Об'єднати шари Merge Shape Components Об'єднати компоненти фігури Merge Visible Об'єднати видимі diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index c05376c1ee..ca091c14d8 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -936,6 +936,7 @@ Disable Layer Mask Tắt mặt nạ lớp Release Clipping Mask Nhả mặt nạ cắt Merge Down Gộp xuống + Merge Group Gộp nhóm Quick Export As PNG Xuất nhanh dạng PNG Rename Layer… Đổi tên lớp… Frame from Layers… Frame từ các lớp… diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 08f00a0fa5..d0309166ce 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -393,6 +393,7 @@ Select Linked Layers 选择链接的图层 Merge Layers 合并图层 Merge Down 向下合并 + Merge Group 合并组 Merge Visible 合并可见图层 Stamp Visible 盖印可见图层 Select Layer Above 选择上方图层 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index f2bcab8538..8698dfc078 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -398,6 +398,7 @@ Select Linked Layers 選取連結的圖層 Merge Layers 合併圖層 Merge Down 向下合併圖層 + Merge Group 合併群組 Merge Visible 合併可見圖層 Stamp Visible 蓋印可見圖層 Select Layer Above 選取上方圖層 diff --git a/crates/ui-egui/src/layer_menu_ui.rs b/crates/ui-egui/src/layer_menu_ui.rs index 6c4c0ec99e..5be01c750a 100644 --- a/crates/ui-egui/src/layer_menu_ui.rs +++ b/crates/ui-egui/src/layer_menu_ui.rs @@ -111,6 +111,9 @@ pub fn entries(l: &Layer, multi: bool, has_selection: bool) -> Vec { v.push(None); if multi { v.push(Some((tl!("Merge Layers"), "layer.mergeLayers"))); + } else if matches!(l.content, LayerContent::Group(_)) { + // Photoshop's Merge Group: a group merges itself, not into the layer below (#3832). + v.push(Some((tl!("Merge Group"), "layer.mergeGroup"))); } else { v.push(Some((tl!("Merge Down"), "layer.mergeDown"))); } @@ -654,6 +657,31 @@ mod tests { assert!(ids.contains(&"layer.mergeLayers")); } + /// #3832: a group's right-click menu and the Layer menu offer Photoshop's Merge Group, which + /// merges the group itself (not into the layer below), and it is enabled. + #[test] + fn a_group_offers_merge_group() { + let mut s = photocraft_engine::Session::new(); + s.execute("file.new", json!({"width": 10, "height": 10})).unwrap(); + s.execute("layer.new.layer", json!({})).unwrap(); + s.execute("layer.groupLayers", json!({"name": "G"})).unwrap(); + let st = s.active().unwrap(); + let g = st.doc.layer(st.active_layer.unwrap()).unwrap().clone(); + let e: Vec<(&str, &str)> = entries(&g, false, false).into_iter().flatten().collect(); + assert!(e.contains(&("Merge Group", "layer.mergeGroup")), "{e:?}"); + assert!(!e.iter().any(|x| x.0 == "Merge Down")); + let mut app = crate::PhotocraftApp::new(s, Default::default()); + app.sync_views(); + let items = crate::menus::menu_items(&app); + let item = items.iter().find(|i| i.id == "layer.mergeLayers").unwrap(); + assert_eq!(item.label, "Merge Group"); + assert!(item.enabled, "Merge Group runs on a group"); + // A plain layer keeps Merge Layers. + app.run("layer.new.layer", json!({})).unwrap(); + let items = crate::menus::menu_items(&app); + assert_eq!(items.iter().find(|i| i.id == "layer.mergeLayers").unwrap().label, "Merge Layers"); + } + #[test] fn add_mask_uses_the_selection() { assert_eq!(add_mask_command(false, false), "layer.layerMask.revealAll"); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 4e6c9ba188..18f1d6afb3 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -966,6 +966,14 @@ pub fn menu_items(app: &PhotocraftApp) -> Vec { { item.label = crate::layer_menu_ui::mask_toggle_label(mask.enabled).into(); } + // Layer › Merge Layers reads Merge Group with just a group selected, as in Photoshop (#3832). + if let Some(st) = app.session.active() + && st.selected_layers().len() <= 1 + && st.active_layer.and_then(|id| st.doc.layer(id)).is_some_and(|l| matches!(l.content, photocraft_doc::LayerContent::Group(_))) + && let Some(item) = items.iter_mut().find(|i| i.id == "layer.mergeLayers") + { + item.label = "Merge Group".into(); + } // The File quick-export command uses the format selected in Export Preferences. // On the web it intentionally downloads PNG until the quick-export service supports // the other formats; the Layer quick-export command also always produces PNG. From 094c4f30e4067bd3eb8564bed53843ac77881e7e Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:43:34 +0200 Subject: [PATCH 07/27] CMS tests: validate profile-specific CMYK black endpoints (#3890) Retain the relative paper-white requirement and round-trip checks while validating finite, bounded endpoints and darker full-K ink. Add an encoded and reparsed AToB1 regression with an independently calculated 100/87/74/61/48 lightness ramp, plus invalid endpoint cases. Signed-off-by: FredApps <153173854+FredApps@users.noreply.github.com> --- crates/cms/tests/cms.rs | 69 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/crates/cms/tests/cms.rs b/crates/cms/tests/cms.rs index ba3f008fb4..f390a390da 100644 --- a/crates/cms/tests/cms.rs +++ b/crates/cms/tests/cms.rs @@ -450,6 +450,73 @@ fn malformed_profiles_are_rejected_without_panicking() { } } +// A press's full-K lightness depends on its ink and paper (newsprint can exceed L* 40). +// Keep the relative paper-white check and require finite endpoints with darker, bounded ink. +fn cmyk_endpoints_valid(paper: [f64; 3], black: [f64; 3]) -> bool { + // Allow only float rounding at the physical L* limits, not a different printing condition. + let lightness = -1e-4..=100.0 + 1e-4; + paper.iter().chain(&black).all(|v| v.is_finite()) + && lightness.contains(&paper[0]) + && lightness.contains(&black[0]) + && paper[0] > 90.0 + && black[0] < paper[0] +} + +#[test] +fn a_light_black_cmyk_lut_has_the_known_relative_ramp() { + use photocraft_cms::Curve; + use photocraft_cms::pipeline::Stage; + use photocraft_cms::profile::{Lut, LutKind}; + + let mut p = cmyk().clone(); + // Independent linear test model: L* = 100 - 52 K, neutral a*/b*. Exercise A2B1 only; + // the inherited coated profile's inverse is not an inverse of this generated model. + p.a2b = [None, None, None]; + p.b2a = [None, None, None]; + p.a2b[1] = Some(Lut { + kind: LutKind::Ab, + inputs: 4, + outputs: 3, + stages: vec![ + Stage::Curves(vec![Curve::Identity; 4]), + Stage::Clut(Clut::sample(vec![2; 4], 3, |ink, out| { + out.copy_from_slice(&enc([100.0 - 52.0 * f64::from(ink[3]), 0.0, 0.0])); + })), + Stage::Curves(vec![Curve::Identity; 3]), + ], + }); + let p = Profile::parse(&p.with_encoded_bytes().to_bytes()).unwrap(); + let paper = to_lab(&p, &[0.0; 4], Intent::RelativeColorimetric); + let black = to_lab(&p, &[0.0, 0.0, 0.0, 1.0], Intent::RelativeColorimetric); + assert!(cmyk_endpoints_valid(paper, black), "{paper:?} {black:?}"); + // Explicit expected values, not the validation predicate: also catch wrong channels, + // intent selection, constant output and incorrect LUT encoding/interpolation. + for (k, expected) in [(0.0, 100.0), (0.25, 87.0), (0.5, 74.0), (0.75, 61.0), (1.0, 48.0)] { + let actual = to_lab(&p, &[0.0, 0.0, 0.0, k], Intent::RelativeColorimetric); + // The writer quantizes the CLUT to 16 bits: at most 0.000763 L* error. + assert!((actual[0] - expected).abs() < 0.001 && actual[1].abs() < 1e-4 && actual[2].abs() < 1e-4, "K={k}: {actual:?}"); + } +} + +#[test] +fn cmyk_endpoints_reject_invalid_values() { + let paper = [100.0, 0.0, 0.0]; + let black = [48.0, 0.0, 0.0]; + for bad in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] { + for channel in 0..3 { + let mut p = paper; + let mut k = black; + p[channel] = bad; + k[channel] = bad; + assert!(!cmyk_endpoints_valid(p, black)); + assert!(!cmyk_endpoints_valid(paper, k)); + } + } + for (p, k) in [(90.0, 48.0), (100.01, 48.0), (100.0, -0.01), (100.0, 100.01), (100.0, 100.0), (99.0, 100.0)] { + assert!(!cmyk_endpoints_valid([p, 0.0, 0.0], [k, 0.0, 0.0]), "L*: paper {p}, black {k}"); + } +} + /// Parses every profile the OS ships (macOS ColorSync; read-only, nothing is copied) and /// converts through each. #[test] @@ -500,7 +567,7 @@ fn system_profiles() { // Real CMYK profile: paper lighter than 100 % K. let w = to_lab(&p, &[0.0; 4], Intent::RelativeColorimetric); let k = to_lab(&p, &[0.0, 0.0, 0.0, 1.0], Intent::RelativeColorimetric); - assert!(w[0] > 90.0 && k[0] < 40.0, "{}: {w:?} {k:?}", path.display()); + assert!(cmyk_endpoints_valid(w, k), "{}: {w:?} {k:?}", path.display()); // And it round-trips through its own BToA. if p.b2a[1].is_some() { let to = Transform::new(lab(), &p, Intent::RelativeColorimetric, false).unwrap(); From 8072636e0f5a74d9e1bd4fca5101b31a23db2607 Mon Sep 17 00:00:00 2001 From: jonathanwxh-cell Date: Mon, 12 Oct 2026 21:46:59 +0800 Subject: [PATCH 08/27] Count video footage in history memory budgets (#3894) Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com> Co-authored-by: Codex --- crates/ops/src/lib.rs | 83 +++++++++++++++++++++++++++++++++++++++++ crates/ops/src/tiles.rs | 7 ++++ 2 files changed, 90 insertions(+) diff --git a/crates/ops/src/lib.rs b/crates/ops/src/lib.rs index 0f29cf8e98..70cbebe0f7 100644 --- a/crates/ops/src/lib.rs +++ b/crates/ops/src/lib.rs @@ -419,6 +419,89 @@ mod tests { assert_eq!(counter.bytes_with(&empty), retained as u128); } + fn video_document(depth: SampleType, frames: usize) -> Document { + let mut doc = Document::new("video", Size::new(64, 64), ColorMode::Rgb, depth); + let mut layer = Layer::raster("Video", doc.pixel_format()); + let footage: Vec<_> = (0..frames) + .map(|i| { + let mut surface = photocraft_doc::Surface::new(doc.pixel_format()); + surface.write_pixel(1, 1, &[i as f32 / frames as f32, 0.0, 0.0, 1.0]); + surface + }) + .collect(); + layer.content = photocraft_doc::LayerContent::Raster(footage[0].clone()); + layer.video = Some(photocraft_doc::VideoData::new(footage, 24.0)); + doc.layers.push(layer); + doc + } + + #[test] + fn video_memory_counts_originals_and_deduplicates_displayed_frames() { + for depth in [SampleType::U8, SampleType::U16, SampleType::F32] { + let mut doc = video_document(depth, 3); + let tile = doc.layers[0].surface().unwrap().tiles().next().unwrap().1.bytes().len(); + let video = doc.layers[0].video.as_mut().unwrap(); + video.original_frames = Some(video.frames.clone()); + // Original footage and the displayed frame alias the stack, except this painted frame. + video.frames[2].write_pixel(1, 1, &[0.0, 1.0, 0.0, 1.0]); + let mut cur = Arc::new(doc); + let mut h = History { max_bytes: usize::MAX, ..History::default() }; + assert_eq!(h.pixel_bytes(&cur), 4 * tile); + for name in ["A", "B"] { + edit(&mut h, &mut cur, "Rename", |doc| doc.name = name.into()); + h.trim(&cur); + } + assert_counter(&h, &cur); + assert_eq!(h.pixel_bytes(&cur), 4 * tile); + assert_eq!(h.unique_bytes(&cur), 0); + // Rasterizing keeps the displayed frame; the rest of the clip survives only in undo. + edit(&mut h, &mut cur, "Rasterize", |doc| doc.layers[0].video = None); + assert_eq!(h.unique_bytes(&cur), 3 * tile); + assert_eq!(h.pixel_bytes(&cur), 4 * tile); + assert_counter(&h, &cur); + cur = h.undo(cur).unwrap().0; + assert_eq!(h.unique_bytes(&cur), 0); + assert_counter(&h, &cur); + cur = h.redo(cur).unwrap().0; + assert_eq!(h.unique_bytes(&cur), 3 * tile); + assert_counter(&h, &cur); + h.clear(); + assert_eq!(h.pixel_bytes(&cur), tile); + } + } + + #[test] + fn video_memory_budget_evicts_deleted_off_playhead_frames() { + for depth in [SampleType::U8, SampleType::U16, SampleType::F32] { + let mut cur = Arc::new(video_document(depth, 6)); + let tile = cur.layers[0].surface().unwrap().tiles().next().unwrap().1.bytes().len(); + let mut h = History::default(); + for _ in 0..4 { + edit(&mut h, &mut cur, "Delete Frame", |doc| { + doc.layers[0].video.as_mut().unwrap().frames.remove(1); + }); + } + assert_eq!(h.pixel_bytes(&cur), 6 * tile); + assert_eq!(h.unique_bytes(&cur), 4 * tile); + h.max_bytes = 3 * tile; + assert_eq!(h.trim(&cur), 3); + assert_eq!(h.past_len(), 1); + assert_eq!(h.pixel_bytes(&cur), 3 * tile); + assert_eq!(h.unique_bytes(&cur), tile); + assert_counter(&h, &cur); + cur = h.undo(cur).unwrap().0; + assert_eq!(cur.layers[0].video.as_ref().unwrap().frames.len(), 3); + assert_counter(&h, &cur); + cur = h.redo(cur).unwrap().0; + assert_eq!(cur.layers[0].video.as_ref().unwrap().frames.len(), 2); + assert_counter(&h, &cur); + // A budget below the remaining clip still keeps the latest step undoable. + h.max_bytes = 1; + assert_eq!(h.trim(&cur), 0); + assert!(h.can_undo()); + } + } + #[test] fn counter_tracks_eviction_undo_redo_branching_purge_and_clones() { for depth in [SampleType::U8, SampleType::U16, SampleType::F32] { diff --git a/crates/ops/src/tiles.rs b/crates/ops/src/tiles.rs index 59d73cb50e..c4178b5b31 100644 --- a/crates/ops/src/tiles.rs +++ b/crates/ops/src/tiles.rs @@ -19,6 +19,13 @@ pub(crate) fn surfaces(doc: &Document, mut visit: impl FnMut(&Surface)) { if let Some(mask) = &layer.mask { visit(&mask.surface); } + if let Some(video) = &layer.video { + // Undo retains the whole clip, not only the displayed frame. Visit aliases too: + // Tiles tracks every owner and counts their shared allocations only once. + for frame in video.frames.iter().chain(video.original_frames.iter().flatten()) { + visit(frame); + } + } } for channel in doc.channels.iter().chain(&doc.quick_mask) { visit(&channel.surface); From a58b6f3d9ef68bc5e993aa0958f73528783fde80 Mon Sep 17 00:00:00 2001 From: jonathanwxh-cell Date: Mon, 12 Oct 2026 21:52:59 +0800 Subject: [PATCH 09/27] Mix filtered selection edges with premultiplied alpha (#3895) * Mix filtered selection edges with premultiplied alpha Co-authored-by: Codex * Update stylize kernel comparison for alpha-aware selection mixing Co-authored-by: Codex --------- Co-authored-by: jonathnwxh <258340642+jonathanwxh-cell@users.noreply.github.com> Co-authored-by: Codex --- crates/algo/src/blur.rs | 5 +- crates/algo/src/blur/motion_apply.rs | 2 +- crates/algo/src/blur/motion_fft_tests.rs | 4 +- crates/algo/src/blur/motion_tests.rs | 2 +- crates/algo/src/lib.rs | 33 +++++++- crates/algo/src/pixelate/tests.rs | 2 +- crates/algo/src/shape_blur_tests.rs | 2 +- crates/algo/src/stylize2/tests.rs | 12 +-- crates/algo/src/tests_iris.rs | 2 +- crates/algo/src/tests_selection_mix.rs | 98 ++++++++++++++++++++++++ crates/algo/src/tests_tilt_shift.rs | 2 +- 11 files changed, 139 insertions(+), 25 deletions(-) create mode 100644 crates/algo/src/tests_selection_mix.rs diff --git a/crates/algo/src/blur.rs b/crates/algo/src/blur.rs index 606ee7dbc5..ae8b12f40a 100644 --- a/crates/algo/src/blur.rs +++ b/crates/algo/src/blur.rs @@ -992,8 +992,9 @@ mod tests { let mixed = crate::apply_in(&s, &p, canvas, canvas, Some(&half), canvas); for (x, y) in [(0, 0), (18, 11), (36, 22)] { let (o, f, m) = (s.pixel(x, y), full.pixel(x, y), mixed.pixel(x, y)); - for c in 0..4 { - assert!((m[c] - (o[c] + f[c]) / 2.0).abs() < 1e-5, "({x},{y}) channel {c}"); + assert!((m[3] - (o[3] + f[3]) / 2.0).abs() < 1e-5, "({x},{y}) alpha"); + for c in 0..3 { + assert!((m[c] * m[3] - (o[c] * o[3] + f[c] * f[3]) / 2.0).abs() < 1e-5, "({x},{y}) premultiplied channel {c}"); } } let cancel = || true; diff --git a/crates/algo/src/blur/motion_apply.rs b/crates/algo/src/blur/motion_apply.rs index cc2f3e84ee..166a1da5a2 100644 --- a/crates/algo/src/blur/motion_apply.rs +++ b/crates/algo/src/blur/motion_apply.rs @@ -169,7 +169,7 @@ fn apply_inner( return None; } if let Some(selection) = selection { - crate::mix_selection(&mut data, output, selection, &src); + crate::mix_selection(&mut data, output, selection, &src, fmt.alpha); } if ctl.cancelled() { return None; diff --git a/crates/algo/src/blur/motion_fft_tests.rs b/crates/algo/src/blur/motion_fft_tests.rs index 12271d7bc2..fd27434bc4 100644 --- a/crates/algo/src/blur/motion_fft_tests.rs +++ b/crates/algo/src/blur/motion_fft_tests.rs @@ -280,7 +280,7 @@ fn motion_fft_pipeline_supports_depths_models_edges_selections_and_tile_seams() for mask in [None, Some(&selection)] { let mut expected_data = filtered.clone(); if let Some(mask) = mask { - crate::mix_selection(&mut expected_data, rect, mask, &src); + crate::mix_selection(&mut expected_data, rect, mask, &src, alpha); } let mut expected = source.clone(); expected.write_region(rect, &expected_data); @@ -362,7 +362,7 @@ fn motion_fft_production_crosses_real_tile_seams_and_leaves_other_pixels_untouch let mut expected = source.clone(); let mut data = filtered.clone(); if let Some(mask) = mask { - crate::mix_selection(&mut data, area, mask, &src); + crate::mix_selection(&mut data, area, mask, &src, true); } expected.write_region(area, &data); let expected = Image::read(&expected, check); diff --git a/crates/algo/src/blur/motion_tests.rs b/crates/algo/src/blur/motion_tests.rs index 822cbb755e..1141dec559 100644 --- a/crates/algo/src/blur/motion_tests.rs +++ b/crates/algo/src/blur/motion_tests.rs @@ -69,7 +69,7 @@ fn row_surface(surface: &Surface, params: &FilterParams, area: Rect, bounds: Rec let ctx = Ctx { bounds, mode: format.mode, alpha: format.alpha }; let mut data = motion_rows(&src, area, &ctx, *angle, *distance); if let Some(selection) = selection { - crate::mix_selection(&mut data, area, selection, &src); + crate::mix_selection(&mut data, area, selection, &src, format.alpha); } let mut result = surface.clone(); result.write_region(area, &data); diff --git a/crates/algo/src/lib.rs b/crates/algo/src/lib.rs index ded4ad31a1..fdbe6dd72b 100644 --- a/crates/algo/src/lib.rs +++ b/crates/algo/src/lib.rs @@ -1095,7 +1095,7 @@ fn apply_tiled_with_impl( return (*t, Vec::new()); }; if let Some(sel) = selection { - mix_selection(&mut data, *t, sel, src); + mix_selection(&mut data, *t, sel, src, fmt.alpha); } tick(); (*t, data) @@ -1179,7 +1179,7 @@ fn apply_box_blur( let write = |(s, v): &mut (Surface, Vec)| { for (t, data) in v.iter_mut() { if let Some(sel) = selection { - mix_selection(data, *t, sel, &Image::read(surface, *t)); + mix_selection(data, *t, sel, &Image::read(surface, *t), fmt.alpha); } s.write_region(*t, data); } @@ -1202,8 +1202,9 @@ fn apply_box_blur( } /// Mixes filtered `data` for `t` back toward the original pixels in `orig` by the selection's -/// coverage (channel 0 of `sel`). -fn mix_selection(data: &mut [f32], t: Rect, sel: &Surface, orig: &Image) { +/// coverage (channel 0 of `sel`). Changing alpha requires premultiplied colour interpolation; +/// otherwise hidden colours leak into feathered edges and alpha-removing filters darken them. +fn mix_selection(data: &mut [f32], t: Rect, sel: &Surface, orig: &Image, alpha: bool) { let w = t.width() as usize; for (i, px) in data.chunks_exact_mut(orig.ch.max(1)).enumerate() { let (x, y) = (t.x0 + (i % w) as i32, t.y0 + (i / w) as i32); @@ -1211,6 +1212,27 @@ fn mix_selection(data: &mut [f32], t: Rect, sel: &Surface, orig: &Image) { if k >= 1.0 { continue; } + if k <= 0.0 { + for (c, v) in px.iter_mut().enumerate() { + *v = orig.get(x, y, c); + } + continue; + } + if alpha { + let a = px.len() - 1; + let original_alpha = orig.get(x, y, a); + let filtered_alpha = px[a]; + if original_alpha != filtered_alpha { + let mixed_alpha = original_alpha + (filtered_alpha - original_alpha) * k; + let original_weight = original_alpha * (1.0 - k); + let filtered_weight = filtered_alpha * k; + for (c, v) in px[..a].iter_mut().enumerate() { + *v = if mixed_alpha > 0.0 { (orig.get(x, y, c) * original_weight + *v * filtered_weight) / mixed_alpha } else { 0.0 }; + } + px[a] = mixed_alpha; + continue; + } + } for (c, v) in px.iter_mut().enumerate() { let o = orig.get(x, y, c); *v = o + (*v - o) * k; @@ -1229,6 +1251,9 @@ mod tests_ext; #[cfg(test)] mod tests_mosaic; +#[cfg(test)] +mod tests_selection_mix; + /// The current upstream sampler through the identical source/selection/tile pipeline. #[cfg(test)] #[allow(clippy::too_many_arguments)] diff --git a/crates/algo/src/pixelate/tests.rs b/crates/algo/src/pixelate/tests.rs index 83d89bf303..c82d747326 100644 --- a/crates/algo/src/pixelate/tests.rs +++ b/crates/algo/src/pixelate/tests.rs @@ -153,7 +153,7 @@ fn tiled_filters_match_scalar_at_all_depths_with_soft_selection() { FilterParams::ColorHalftone { max_radius, angles } => reference::halftone(&image, bounds, &ctx, max_radius, angles), _ => Vec::new(), }; - crate::mix_selection(&mut expected_data, bounds, &selection, &image); + crate::mix_selection(&mut expected_data, bounds, &selection, &image, ctx.alpha); let mut expected = surface.clone(); expected.write_region(bounds, &expected_data); for tile in [1, 7, 17, 256] { diff --git a/crates/algo/src/shape_blur_tests.rs b/crates/algo/src/shape_blur_tests.rs index 96f8f4c1e8..02bdb0b3ca 100644 --- a/crates/algo/src/shape_blur_tests.rs +++ b/crates/algo/src/shape_blur_tests.rs @@ -129,7 +129,7 @@ fn surfaces_exact_depth_models_alpha_selection_and_tile_boundaries() { }; let spans = Spans::new(radius, shape_inside(shape)); let mut expected = conv_spans(&src, area, &ctx, &spans); - crate::mix_selection(&mut expected, area, &selection, &src); + crate::mix_selection(&mut expected, area, &selection, &src, alpha); let mut reference = surface.clone(); reference.write_region(area, &expected); for tile in [17, 256] { diff --git a/crates/algo/src/stylize2/tests.rs b/crates/algo/src/stylize2/tests.rs index cc44558ea6..3d7f6b5235 100644 --- a/crates/algo/src/stylize2/tests.rs +++ b/crates/algo/src/stylize2/tests.rs @@ -167,17 +167,7 @@ fn storage_selection_and_tile_sizes_match_original_kernels() { FilterParams::Wind { method, from_right, seed } => wind_direct(&src, area, &ctx, method, from_right, seed), _ => continue, }; - let original = s.read_region(area); - for (i, px) in raw.chunks_exact_mut(fmt.channels()).enumerate() { - let (x, y) = (area.x0 + (i % area.width() as usize) as i32, area.y0 + (i / area.width() as usize) as i32); - let k = selection.sample_channel(x, y, 0); - if k >= 1.0 { - continue; - } - for (c, v) in px.iter_mut().enumerate() { - *v = original[i * fmt.channels() + c] + (*v - original[i * fmt.channels() + c]) * k; - } - } + crate::mix_selection(&mut raw, area, &selection, &src, fmt.alpha); let mut want = s.clone(); want.write_region(area, &raw); for tile in [1, 7, 17, 256] { diff --git a/crates/algo/src/tests_iris.rs b/crates/algo/src/tests_iris.rs index 1f930c172d..2d54c8f134 100644 --- a/crates/algo/src/tests_iris.rs +++ b/crates/algo/src/tests_iris.rs @@ -170,7 +170,7 @@ fn iris_depths_models_selection_tiles_and_cancellation() { let t = Rect::new(x, y, (x + tile).min(area.x1), (y + tile).min(area.y1)); let src = Image::read_clamped(&surface, t.inflate(radius), bounds); let mut data = reference::iris(&src, t, &ctx, &pins); - crate::mix_selection(&mut data, t, &selection, &src); + crate::mix_selection(&mut data, t, &selection, &src, alpha); want.write_region(t, &data); } } diff --git a/crates/algo/src/tests_selection_mix.rs b/crates/algo/src/tests_selection_mix.rs new file mode 100644 index 0000000000..f715a8bbc2 --- /dev/null +++ b/crates/algo/src/tests_selection_mix.rs @@ -0,0 +1,98 @@ +use super::*; +use photocraft_color::{PixelFormat, SampleType}; + +fn selection(coverage: f32) -> Surface { + Surface::with_default(PixelFormat::new(ColorMode::Grayscale, SampleType::F32, false), &[coverage]) +} + +#[test] +fn feathered_color_to_alpha_preserves_appearance_over_the_key() { + let r = Rect::new(0, 0, 1, 1); + let filter = FilterParams::ColorToAlpha { color: [1.0; 4], transparency_threshold: 0.0, opacity_threshold: 1.0 }; + for sample in SampleType::ALL { + for mode in [ColorMode::Rgb, ColorMode::Grayscale] { + for alpha in [0.5, 1.0] { + let format = PixelFormat::new(mode, sample, true); + let mut s = Surface::new(format); + let mut p = vec![0.5; format.channels()]; + *p.last_mut().unwrap() = alpha; + s.write_pixel(0, 0, &p); + let original = s.pixel(0, 0); + let a = format.channels() - 1; + for coverage in [0.0, 0.25, 0.5, 0.75, 1.0] { + let out = apply(&s, &filter, r, r, Some(&selection(coverage))); + let actual = out.pixel(0, 0); + let tolerance = if sample == SampleType::U8 { 2.0 / 255.0 } else { 1e-4 }; + for c in 0..a { + let before = original[c] * original[a] + 1.0 - original[a]; + let after = actual[c] * actual[a] + 1.0 - actual[a]; + assert!( + (before - after).abs() < tolerance, + "{sample:?}/{mode:?}, alpha={alpha}, coverage={coverage}: {original:?} -> {actual:?}, over white {before} -> {after}" + ); + } + } + } + } + } +} + +#[test] +fn feathered_offset_ignores_hidden_colour_of_the_original_pixel() { + let r = Rect::new(0, 0, 2, 1); + let filter = FilterParams::Offset { horizontal: 1, vertical: 0, undefined: UndefinedAreas::Wrap }; + for sample in SampleType::ALL { + let mut s = Surface::new(PixelFormat::new(ColorMode::Rgb, sample, true)); + s.write_pixel(0, 0, &[1.0, 0.0, 0.0, 0.0]); + s.write_pixel(1, 0, &[0.0, 0.0, 1.0, 1.0]); + let out = apply(&s, &filter, r, r, Some(&selection(0.5))); + let p = out.pixel(0, 0); + assert_eq!(&p[..3], &[0.0, 0.0, 1.0], "{sample:?}: hidden red contaminated the selected blue: {p:?}"); + assert!((p[3] - 0.5).abs() < 1.0 / 255.0); + } +} + +#[test] +fn feathered_blurs_do_not_reintroduce_hidden_colour() { + let r = Rect::new(0, 0, 3, 3); + let mut s = Surface::new(PixelFormat::RGBA32F); + s.fill_rect(r, &[1.0, 0.0, 0.0, 0.0]); + s.write_pixel(1, 1, &[0.0, 0.0, 1.0, 1.0]); + for filter in [FilterParams::BoxBlur { radius: 1.0 }, FilterParams::MotionBlur { angle: 0.0, distance: 3.0 }] { + let full = apply(&s, &filter, r, r, None); + let out = apply(&s, &filter, r, r, Some(&selection(0.5))); + let p = out.pixel(0, 1); + assert!(p[3] > 0.0, "{filter:?}: expected blur to spread coverage"); + assert!((p[3] - full.pixel(0, 1)[3] * 0.5).abs() < 1e-6, "{filter:?}: selection should halve the new coverage"); + assert!(p[0].abs() < 1e-6 && (p[2] - 1.0).abs() < 1e-6, "{filter:?}: hidden red reappeared: {p:?}"); + } +} + +#[test] +fn selection_mixing_keeps_hdr_range_and_exact_endpoints() { + let r = Rect::new(0, 0, 1, 1); + let original = Image { rect: r, ch: 4, data: vec![-2.0, 4.0, 0.5, 0.25] }; + let filtered = vec![8.0, -4.0, 0.75, 1.0]; + let mut data = filtered.clone(); + mix_selection(&mut data, r, &selection(0.5), &original, true); + for (&actual, expected) in data.iter().zip([6.0, -2.4, 0.7, 0.625]) { + assert!((actual - expected).abs() < 1e-6, "{data:?}"); + } + for coverage in [0.0, 1.0] { + let mut data = filtered.clone(); + mix_selection(&mut data, r, &selection(coverage), &original, true); + assert_eq!(data, if coverage == 0.0 { &original.data } else { &filtered }.clone()); + } +} + +#[test] +fn selection_mixing_without_alpha_keeps_every_channel_independent() { + let r = Rect::new(0, 0, 1, 1); + // One channel is a mask; three/four channels can be RGB or CMYK without alpha. + for ch in [1, 3, 4] { + let original = Image { rect: r, ch, data: vec![0.25; ch] }; + let mut data = vec![0.75; ch]; + mix_selection(&mut data, r, &selection(0.25), &original, false); + assert_eq!(data, vec![0.375; ch]); + } +} diff --git a/crates/algo/src/tests_tilt_shift.rs b/crates/algo/src/tests_tilt_shift.rs index 49a6e79bbf..88066ca9d6 100644 --- a/crates/algo/src/tests_tilt_shift.rs +++ b/crates/algo/src/tests_tilt_shift.rs @@ -177,7 +177,7 @@ fn tilt_shift_depths_models_selection_and_tile_edges_match_reference() { None => Image::read(&surface, t.inflate(radius)), }; let mut data = reference::tilt_shift(&src, t, &ctx, 17.0, (0.3, 0.7), 29.0, 0.07, 0.3); - crate::mix_selection(&mut data, t, &selection, &src); + crate::mix_selection(&mut data, t, &selection, &src, alpha); want.write_region(t, &data); } } From bb17dd555c161dbc6ca20c4cf856d1bf713456e5 Mon Sep 17 00:00:00 2001 From: HEOJUNFO <100739943+HEOJUNFO@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:57:11 +0900 Subject: [PATCH 10/27] Color: convert input-only embedded profiles to the working space on open (#3876) (#3900) An iPhone photo embeds Apple's Wide Color Sharing Profile, an input-class profile with only AToB tables. Preserve kept it as the document's space, so every composite into the document failed with "RGB profile without matrix/TRC or BToA tags". Such a profile can't be a destination, so open converts from it to the working space whatever the policy (Off still discards it) and the report says inputOnly. Co-authored-by: Claude Opus 5.5 (1M context) --- crates/cms/src/profile.rs | 7 ++++ crates/engine/src/color_cmds.rs | 62 +++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/crates/cms/src/profile.rs b/crates/cms/src/profile.rs index 6d646cc1e1..99f5710e5d 100644 --- a/crates/cms/src/profile.rs +++ b/crates/cms/src/profile.rs @@ -296,6 +296,13 @@ impl Profile { self.a2b.iter().all(Option::is_none) && (self.matrix.is_some() && self.trc.is_some() || self.gray_trc.is_some()) } + /// Whether colours can be converted into this profile for every intent. Input profiles with + /// only AToB tables (Apple's Wide Color Sharing Profile on iPhone photos) can't: they + /// describe a source but have no PCS → device path. + pub fn can_be_destination(&self) -> bool { + [Intent::Perceptual, Intent::RelativeColorimetric, Intent::Saturation].into_iter().all(|i| self.pcs_to_device(i).is_ok()) + } + fn lut_for(luts: &[Option; 3], intent: Intent) -> Option<&Lut> { let i = match intent { Intent::Perceptual => 0, diff --git a/crates/engine/src/color_cmds.rs b/crates/engine/src/color_cmds.rs index f6d4b1b18c..a68188bd3f 100644 --- a/crates/engine/src/color_cmds.rs +++ b/crates/engine/src/color_cmds.rs @@ -276,18 +276,22 @@ impl ColorState { if !mismatch || linear_hdr { return merge(base, json!({"action": "kept"})); } + // An input-only profile can't be the document's space: nothing could be + // composited into it (#3876). Preserve converts it to the working space instead. + let input_only = !emb.can_be_destination(); let action = match policy { - Policy::Preserve => "kept", + Policy::Preserve if !input_only => "kept", Policy::Off => { doc.icc_profile = None; "discarded" } - Policy::Convert => match convert_document(doc, &working, self.settings.intent(), self.settings.bpc) { + Policy::Preserve | Policy::Convert => match convert_document(doc, &working, self.settings.intent(), self.settings.bpc) { Ok(()) => "converted", Err(_) => "kept", }, }; - merge(base, json!({"action": action, "ask": self.settings.ask_on_mismatch})) + let report = merge(base, json!({"action": action, "ask": self.settings.ask_on_mismatch})); + if input_only { merge(report, json!({"inputOnly": true})) } else { report } } None => { // Untagged files are assumed to be in the working space; tag them when it is not @@ -1397,6 +1401,58 @@ mod settings_tests { assert_eq!(desc(&s.active().unwrap().doc).as_deref(), Some("Display P3")); } + /// Display P3 as an input profile with only an AToB table, like Apple's Wide Color Sharing + /// Profile embedded in iPhone photos. + fn input_only_p3() -> Profile { + let p3 = Builtin::DisplayP3.profile(); + let t = Transform::new(p3, Builtin::LabD50.profile(), Intent::RelativeColorimetric, false).unwrap(); + let clut = photocraft_cms::Clut::sample(vec![9; 3], 3, |inp, out| t.eval(inp, out)); + let ident = || photocraft_cms::pipeline::Stage::Curves(vec![photocraft_cms::Curve::Identity; 3]); + let lut = photocraft_cms::profile::Lut { + kind: photocraft_cms::profile::LutKind::Ab, + inputs: 3, + outputs: 3, + stages: vec![ident(), photocraft_cms::pipeline::Stage::Clut(clut), ident()], + }; + let mut p = p3.clone(); + p.class = photocraft_cms::ProfileClass::Input; + p.pcs = photocraft_cms::Pcs::Lab; + p.description = "Input-only P3".into(); + (p.matrix, p.trc) = (None, None); + p.a2b = [Some(lut), None, None]; + p.b2a = [None, None, None]; + p.with_encoded_bytes() + } + + #[test] + fn input_only_profiles_are_converted_on_open() { + // #3876: an iPhone photo's input-only profile was kept as the document's space, so + // compositing anything into it failed with "RGB profile without matrix/TRC or BToA tags". + let input = input_only_p3(); + assert!(Builtin::DisplayP3.profile().can_be_destination()); + assert!(!input.can_be_destination()); + let mut s = Session::new(); + let mut d = tagged("srgb"); + d.icc_profile = Some(input.to_bytes()); + let (photo, r) = s.open_document(d, None); + assert_eq!((r["action"].as_str(), r["inputOnly"].as_bool(), r["ask"].as_bool()), (Some("converted"), Some(true), Some(true))); + let doc = &s.active().unwrap().doc; + assert!(desc(doc).unwrap().starts_with("sRGB")); + // Converted from P3, not reinterpreted as sRGB. + assert!((doc.layers[0].surface().unwrap().pixel(1, 1)[0] - 0.2).abs() > 0.01); + let (other, _) = s.open_document(tagged("srgb"), None); + s.execute("layer.copyToDocument", json!({"document": photo, "source": other})).unwrap(); + // Off still discards the profile, and profiles that can be a destination are still kept. + s.execute("edit.colorSettings", json!({"policyRgb": "off"})).unwrap(); + let mut d = tagged("srgb"); + d.icc_profile = Some(input.to_bytes()); + let (_, r) = s.open_document(d, None); + assert_eq!((r["action"].as_str(), r["inputOnly"].as_bool()), (Some("discarded"), Some(true))); + s.execute("edit.colorSettings", json!({"policyRgb": "preserve"})).unwrap(); + let (_, r) = s.open_document(tagged("display-p3"), None); + assert_eq!((r["action"].as_str(), r.get("inputOnly")), (Some("kept"), None)); + } + /// A little-endian EXIF block whose Exif IFD holds ColorSpace = `space`. fn exif_color_space(space: u16) -> Vec { let mut b = b"Exif\0\0II\x2a\0\x08\0\0\0".to_vec(); From 5a0e2e26582c6aac610eb077e28e55a5fe3a20a6 Mon Sep 17 00:00:00 2001 From: JGF <44821497+bommerts@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:26:29 -0400 Subject: [PATCH 11/27] Web: wasm back under the 24 MiB gate with ~1 MiB headroom (25.38 -> 24.17 MB) The "web build and wasm size gate" job failed on main and every PR: the release `_bg.wasm` measured 25,379,677 bytes at 8d49ecf5, over the 25,165,824-byte limit in packaging/web/package.sh. - egui's default fonts (Ubuntu Light, Hack, Noto Emoji, emoji-icon-font) were embedded uncompressed (1.4 MB) by egui's `default_fonts` feature. The app now builds egui without it; ui-egui's build.rs deflates the same files from the `epaint_default_fonts` crate (a build-dependency, so no font files are committed) and `egui_fonts.rs` rebuilds egui's exact default stack from them on first use. Test builds keep the feature as the oracle: the rebuilt definitions equal egui's byte for byte. - Translation catalogs: every catalog repeated the same English sources. build.rs now stores the `contextsource` keys once and each catalog as references plus translations, losslessly (`text()` still returns each .tsv byte for byte; malformed lines are kept as they are). - wasm-release profile: opt-level "z" for PhotoCraft's own crates. Dependencies stay at "s" (at "z" the wasm grew 0.4 MB, measured), the file-decoding crates (codecs, psd, raw, heif, vector) stay at "s" and the pixel crates at 3, so decoding and filters keep their speed. Measured with `trunk build --release` (trunk 0.21.14, wasm-opt v123 -Oz): upstream main 8d49ecf5 25,379,677 bytes this change 24,167,186 bytes (-1,212,491) gate 25,165,824 bytes (998,638 headroom) Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + Cargo.toml | 40 +++++----- apps/photocraft-web/Cargo.toml | 2 +- apps/photocraft/Cargo.toml | 2 +- crates/ui-egui/Cargo.toml | 9 ++- crates/ui-egui/build.rs | 129 ++++++++++++++++++++++++++----- crates/ui-egui/src/egui_fonts.rs | 91 ++++++++++++++++++++++ crates/ui-egui/src/i18n/mod.rs | 70 +++++++++++++++-- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/theme.rs | 3 +- packaging/web/README.md | 17 ++-- xtask/src/i18n_coverage.rs | 2 +- 12 files changed, 310 insertions(+), 57 deletions(-) create mode 100644 crates/ui-egui/src/egui_fonts.rs diff --git a/Cargo.lock b/Cargo.lock index 160403c482..404c51f354 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4574,6 +4574,7 @@ dependencies = [ "egui", "egui_extras", "egui_kittest", + "epaint_default_fonts", "flate2", "log", "photocraft-algo", diff --git a/Cargo.toml b/Cargo.toml index 87e0a378ce..5c003659b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,9 @@ zune-core = "0.5" zune-jpeg = "0.5" proptest = "1" eframe = { version = "0.36", default-features = false } -egui = "0.36" +# Without `default_fonts`, which embeds egui's fonts uncompressed: ui-egui ships them deflated +# (egui_fonts.rs; the web build's size gate). +egui = { version = "0.36", default-features = false } egui_kittest = "0.36" rfd = "0.17" log = "0.4" @@ -81,13 +83,14 @@ codegen-units = 1 # The browser build (apps/photocraft-web, `data-cargo-profile` in its index.html). Hosts cap the # size of a single file (Cloudflare Pages/Workers: 25 MiB), and the wasm is downloaded on every -# visit, so this profile optimizes for size: fat LTO, opt-level "z" for the UI, command, and I/O -# shells, and "s" for other crates except pixel processing, which keeps opt-level 3. panic -# stays the target default (wasm32-unknown-unknown aborts; AGENTS.md's `panic = "unwind"` is for -# the native app shell). packaging/web/package.sh fails the build above 24 MiB. +# visit, so this profile optimizes for size: fat LTO, opt-level "z" for PhotoCraft's own crates, +# "s" for dependencies and for the crates that decode files, and 3 for pixel processing (filters +# and compositing). Dependencies stay at "s": at "z" the whole wasm grows (by 0.4 MB, measured). +# panic stays the target default (wasm32-unknown-unknown aborts; AGENTS.md's `panic = "unwind"` +# is for the native app shell). packaging/web/package.sh fails the build above 24 MiB. [profile.wasm-release] inherits = "release" -opt-level = "s" +opt-level = "z" lto = "fat" codegen-units = 1 strip = "debuginfo" @@ -95,20 +98,19 @@ strip = "debuginfo" [profile.wasm-release.package."*"] opt-level = "s" -# Keep the browser shell compact as shared panel controls grow. Pixel-processing crates below -# retain their speed-oriented optimization level. -[profile.wasm-release.package.photocraft-ui-egui] -opt-level = "z" - -# Keep browser command dispatch, import/export glue, and the entry point compact. Native -# builds and the speed-oriented pixel-processing overrides below retain their settings. -[profile.wasm-release.package.photocraft-engine] -opt-level = "z" -[profile.wasm-release.package.photocraft-io] -opt-level = "z" -[profile.wasm-release.package.photocraft-web] -opt-level = "z" +# Decoding and rasterizing keep their speed. +[profile.wasm-release.package.photocraft-codecs] +opt-level = "s" +[profile.wasm-release.package.photocraft-psd] +opt-level = "s" +[profile.wasm-release.package.photocraft-raw] +opt-level = "s" +[profile.wasm-release.package.photocraft-heif] +opt-level = "s" +[profile.wasm-release.package.photocraft-vector] +opt-level = "s" +# Pixel processing keeps its speed-oriented optimization level. [profile.wasm-release.package.photocraft-raster] opt-level = 3 [profile.wasm-release.package.photocraft-color] diff --git a/apps/photocraft-web/Cargo.toml b/apps/photocraft-web/Cargo.toml index 6b4c62797a..a4ded64181 100644 --- a/apps/photocraft-web/Cargo.toml +++ b/apps/photocraft-web/Cargo.toml @@ -29,7 +29,7 @@ log = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] # `wgpu` pulls egui-wgpu's defaults: WebGPU plus the WebGL2 fallback. -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] } +eframe = { workspace = true, features = ["wgpu", "accesskit"] } rfd = { workspace = true } wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" diff --git a/apps/photocraft/Cargo.toml b/apps/photocraft/Cargo.toml index 740a75b3f4..57b537931c 100644 --- a/apps/photocraft/Cargo.toml +++ b/apps/photocraft/Cargo.toml @@ -29,7 +29,7 @@ photocraft-automation = { workspace = true } photocraft-ui-egui = { workspace = true } # Pen tablet pressure/tilt on macOS and X11 (the workspace's one isolated unsafe helper crate). photocraft-tablet = { workspace = true } -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11", "persistence"] } +eframe = { workspace = true, features = ["wgpu", "accesskit", "wayland", "x11", "persistence"] } egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index 568727163b..d6a49c0038 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -30,7 +30,7 @@ photocraft-format = { workspace = true } photocraft-codecs = { workspace = true } egui = { workspace = true } egui_extras = { version = "0.36", default-features = false, features = ["svg"] } -eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] } +eframe = { workspace = true, features = ["wgpu", "accesskit"] } serde = { workspace = true } serde_json = { workspace = true } log = { workspace = true } @@ -49,6 +49,9 @@ sys-locale = "=0.3.2" winsafe = { version = "=0.0.29", default-features = false, features = ["kernel"] } [dev-dependencies] +# egui's own default fonts in tests: the oracle for egui_fonts.rs, and contexts made with +# `Context::default()` (no install_fonts) can lay out text. +egui = { workspace = true, features = ["default_fonts"] } # Synthetic raw files for the open-time Camera Raw dialog tests. photocraft-raw = { workspace = true, features = ["testgen"] } egui_kittest = { workspace = true, features = ["wgpu", "snapshot", "eframe"] } @@ -58,7 +61,9 @@ photocraft-testkit = { workspace = true } photocraft-psd = { workspace = true } [build-dependencies] -# Deflates the translation catalogs (build.rs); already linked at run time to inflate them. +# Deflates the translation catalogs and egui's default fonts (build.rs); already linked at run time to inflate them. flate2 = { workspace = true } +# egui's default fonts, deflated by build.rs for egui_fonts.rs (the same version egui uses). +epaint_default_fonts = "0.36" serde_json = { workspace = true } toml = "1" diff --git a/crates/ui-egui/build.rs b/crates/ui-egui/build.rs index fa0e19cc1f..900370eb69 100644 --- a/crates/ui-egui/build.rs +++ b/crates/ui-egui/build.rs @@ -8,10 +8,10 @@ //! //! A missing or malformed file gives empty tables (or no names) and a warning. //! -//! Also deflates the translation catalogs (`src/i18n/*.tsv`) into `OUT_DIR/i18n/.tsv.deflate` -//! for `i18n::LANGUAGES`: stored compressed they take ~0.65 MB of the binary instead of ~1.9 MB, -//! which keeps the web build under its 24 MiB size gate (packaging/web/package.sh). A catalog is -//! inflated the first time its language is used. +//! Also packs the translation catalogs (`src/i18n/*.tsv`) into `OUT_DIR/i18n/` for +//! `i18n::LANGUAGES` (see [`pack_catalogs`]): stored that way they take ~0.5 MB of the binary +//! instead of ~2.2 MB, which keeps the web build under its 24 MiB size gate +//! (packaging/web/package.sh). A catalog is unpacked the first time its language is used. use std::fmt::Write as _; @@ -27,30 +27,123 @@ fn main() { if let Err(e) = std::fs::write(&out, generate(&json, &people)) { println!("cargo::warning=writing {}: {e}", out.display()); } - deflate_catalogs(); + pack_catalogs(); + deflate_egui_fonts(); } -/// Deflate every `src/i18n/*.tsv` into `OUT_DIR/i18n/`. A catalog that can't be written is a -/// build error (`include_bytes!` would fail on it anyway, with a less helpful message). -fn deflate_catalogs() { +/// Deflate egui's default fonts (the `epaint_default_fonts` crate) into `OUT_DIR/egui_fonts/` for +/// `egui_fonts.rs`. The app builds egui without `default_fonts`, which would embed the same 1.4 MB +/// uncompressed; deflated they take 0.78 MB (the web build's 24 MiB size gate). +fn deflate_egui_fonts() { use std::io::Write as _; - let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/i18n"); - let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("i18n"); - println!("cargo::rerun-if-changed={}", src.display()); + let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("egui_fonts"); + let fonts = [ + ("Hack-Regular", epaint_default_fonts::HACK_REGULAR), + ("NotoEmoji-Regular", epaint_default_fonts::NOTO_EMOJI_REGULAR), + ("Ubuntu-Light", epaint_default_fonts::UBUNTU_LIGHT), + ("emoji-icon-font", epaint_default_fonts::EMOJI_ICON), + ]; let result = (|| -> std::io::Result<()> { std::fs::create_dir_all(&out)?; - for entry in std::fs::read_dir(&src)? { - let path = entry?.path(); - let Some(name) = path.file_name().and_then(|n| n.to_str()).filter(|n| n.ends_with(".tsv")) else { continue }; - println!("cargo::rerun-if-changed={}", path.display()); + for (name, bytes) in fonts { let mut z = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best()); - z.write_all(&std::fs::read(&path)?)?; - std::fs::write(out.join(format!("{name}.deflate")), z.finish()?)?; + z.write_all(bytes)?; + std::fs::write(out.join(format!("{name}.ttf.deflate")), z.finish()?)?; } Ok(()) })(); if let Err(e) = result { - println!("cargo::error=deflating the translation catalogs in {}: {e}", src.display()); + println!("cargo::error=deflating egui's default fonts into {}: {e}", out.display()); + } +} + +/// Whether a catalog line is an entry (`contextsourcetranslation`, source and +/// translation non-empty, as `i18n::catalog::parse_entries` reads it), and its key and translation. +fn catalog_entry(line: &str) -> Option<(&str, &str)> { + if line.starts_with('#') { + return None; + } + let mut cols = line.splitn(3, '\t'); + let (ctx, src, tr) = (cols.next()?, cols.next()?, cols.next()?); + let key = line.get(..ctx.len() + 1 + src.len())?; + (!src.is_empty() && !tr.is_empty() && !tr.contains('\t')).then_some((key, tr)) +} + +/// Pack every `src/i18n/*.tsv` into `OUT_DIR/i18n/`, losslessly (`i18n::unpack` restores each file +/// byte for byte). Every catalog repeats the same English sources, so they are stored once: +/// +/// - `keys.deflate`: every entry's `contextsource`, one per line, in order of first +/// appearance (the largest catalog first). +/// - `.tsv.deflate`: the catalog, each entry line replaced by `\x01` when its key +/// follows the previous entry's in `keys`, else `\x02` (the offset from +/// there). Other lines are kept; one that starts with \x01, \x02 or \x03 gets a \x03 prefix. +/// +/// Both are deflated. A catalog that can't be written is a build error (`include_bytes!` would +/// fail on it anyway, with a less helpful message). +fn pack_catalogs() { + use std::io::Write as _; + let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/i18n"); + let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap_or_default()).join("i18n"); + println!("cargo::rerun-if-changed={}", src.display()); + let deflate = |bytes: &[u8]| -> std::io::Result> { + let mut z = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::best()); + z.write_all(bytes)?; + z.finish() + }; + let result = (|| -> std::io::Result<()> { + std::fs::create_dir_all(&out)?; + let mut catalogs = Vec::new(); + for entry in std::fs::read_dir(&src)? { + let path = entry?.path(); + let Some(name) = path.file_name().and_then(|n| n.to_str()).filter(|n| n.ends_with(".tsv")) else { continue }; + println!("cargo::rerun-if-changed={}", path.display()); + catalogs.push((name.to_owned(), std::fs::read_to_string(&path)?)); + } + // Largest first, then by name: the key order (and so the output) is reproducible. + catalogs.sort_by(|a, b| b.1.len().cmp(&a.1.len()).then_with(|| a.0.cmp(&b.0))); + let mut keys: Vec<&str> = Vec::new(); + let mut index = std::collections::HashMap::new(); + for line in catalogs.iter().flat_map(|(_, text)| text.split('\n')) { + if let Some((key, _)) = catalog_entry(line) { + index.entry(key).or_insert_with(|| { + keys.push(key); + keys.len() - 1 + }); + } + } + std::fs::write(out.join("keys.deflate"), deflate(keys.join("\n").as_bytes())?)?; + for (name, text) in &catalogs { + let mut packed = String::with_capacity(text.len()); + let mut next = 0usize; + for (n, line) in text.split('\n').enumerate() { + if n > 0 { + packed.push('\n'); + } + match catalog_entry(line) { + Some((key, tr)) => { + let i = index.get(key).copied().unwrap_or_default(); + if i == next { + packed.push('\x01'); + } else { + let _ = write!(packed, "\x02{}\t", i as i64 - next as i64); + } + packed.push_str(tr); + next = i + 1; + } + None => { + if line.starts_with(['\x01', '\x02', '\x03']) { + packed.push('\x03'); + } + packed.push_str(line); + } + } + } + std::fs::write(out.join(format!("{name}.deflate")), deflate(packed.as_bytes())?)?; + } + Ok(()) + })(); + if let Err(e) = result { + println!("cargo::error=packing the translation catalogs in {}: {e}", src.display()); } } diff --git a/crates/ui-egui/src/egui_fonts.rs b/crates/ui-egui/src/egui_fonts.rs new file mode 100644 index 0000000000..4c71705e64 --- /dev/null +++ b/crates/ui-egui/src/egui_fonts.rs @@ -0,0 +1,91 @@ +//! egui's default fonts (Ubuntu Light, Hack, Noto Emoji and emoji-icon-font: the fallback stack +//! behind Inter and JetBrains Mono, for symbols and emoji), stored deflated. +//! +//! egui's `default_fonts` feature embeds them uncompressed (1.4 MB), which the web build's 24 MiB +//! size gate (packaging/web/package.sh) can't afford. The app builds egui without it; build.rs +//! deflates the same files from the `epaint_default_fonts` crate (0.78 MB) and they are inflated +//! once, the first time the fonts are installed. Test builds keep the feature (dev-dependencies), +//! so egui's own definitions are the oracle for [`definitions`]. + +use std::sync::{Arc, LazyLock}; + +use egui::{FontData, FontDefinitions, FontFamily, FontTweak}; + +/// A font deflated by build.rs. +macro_rules! deflated { + ($name:literal) => { + include_bytes!(concat!(env!("OUT_DIR"), "/egui_fonts/", $name, ".ttf.deflate")) + }; +} + +static HACK: LazyLock> = LazyLock::new(|| inflate(deflated!("Hack-Regular"))); +static NOTO_EMOJI: LazyLock> = LazyLock::new(|| inflate(deflated!("NotoEmoji-Regular"))); +static UBUNTU_LIGHT: LazyLock> = LazyLock::new(|| inflate(deflated!("Ubuntu-Light"))); +static EMOJI_ICON: LazyLock> = LazyLock::new(|| inflate(deflated!("emoji-icon-font"))); + +/// Inflated fonts are bounded (the largest is 0.42 MB), so corrupt data can't exhaust memory. +const MAX_FONT_BYTES: u64 = 16 << 20; + +/// Inflate a font. Data that doesn't inflate gives empty bytes (the font is left out), never a panic. +fn inflate(deflated: &[u8]) -> Vec { + use std::io::Read as _; + let mut out = Vec::new(); + match flate2::read::DeflateDecoder::new(deflated).take(MAX_FONT_BYTES + 1).read_to_end(&mut out) { + Ok(_) if out.len() as u64 <= MAX_FONT_BYTES => out, + _ => Vec::new(), + } +} + +/// `FontDefinitions::default()` with egui's default fonts, whether or not egui was built with +/// `default_fonts`: the same faces, tweaks and family order. +pub fn definitions() -> FontDefinitions { + let fonts = FontDefinitions::default(); + if fonts.font_data.is_empty() { from_deflated() } else { fonts } +} + +/// egui's default definitions, built from the deflated copies. +fn from_deflated() -> FontDefinitions { + let mut fonts = FontDefinitions::empty(); + let faces: [(&str, &'static [u8], f32); 4] = [ + ("Hack", HACK.as_slice(), 1.0), + // egui draws its emoji fonts a little smaller. + ("NotoEmoji-Regular", NOTO_EMOJI.as_slice(), 0.81), + ("Ubuntu-Light", UBUNTU_LIGHT.as_slice(), 1.0), + ("emoji-icon-font", EMOJI_ICON.as_slice(), 0.90), + ]; + for (name, bytes, scale) in faces { + if bytes.is_empty() { + log::error!("egui default font {name} is unavailable"); + continue; + } + let data = FontData::from_static(bytes).tweak(FontTweak { scale, ..Default::default() }); + fonts.font_data.insert(name.to_owned(), Arc::new(data)); + } + let stack = |names: &[&str]| -> Vec { names.iter().filter(|n| fonts.font_data.contains_key(**n)).map(|n| (*n).to_owned()).collect() }; + let mono = stack(&["Hack", "Ubuntu-Light", "NotoEmoji-Regular", "emoji-icon-font"]); + let proportional = stack(&["Ubuntu-Light", "NotoEmoji-Regular", "emoji-icon-font"]); + fonts.families.insert(FontFamily::Monospace, mono); + fonts.families.insert(FontFamily::Proportional, proportional); + fonts +} + +#[cfg(test)] +mod tests { + use super::*; + + /// The definitions built from the deflated copies are egui's own: the same bytes, tweaks and + /// family order (test builds enable `default_fonts`, so `FontDefinitions::default()` is egui's). + #[test] + fn deflated_fonts_match_egui_defaults() { + let egui = FontDefinitions::default(); + assert!(!egui.font_data.is_empty(), "test builds enable egui's default_fonts (dev-dependencies)"); + assert_eq!(from_deflated(), egui); + assert_eq!(definitions(), egui); + } + + #[test] + fn corrupt_font_data_is_left_out_not_a_panic() { + assert!(inflate(b"not deflate data").is_empty()); + assert!(inflate(&[]).is_empty()); + } +} diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index ecf21f46d0..e7347ca5af 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -115,8 +115,8 @@ fn plural_polish(n: u64) -> usize { } } -/// A catalog file as deflated by build.rs (the binary would otherwise carry ~1.9 MB of -/// translations, which the web build's size gate can't afford). +/// A catalog file as packed by build.rs (`pack_catalogs`; the binary would otherwise carry ~2.2 MB +/// of translations, which the web build's size gate can't afford). See [`unpack`]. macro_rules! catalog { ($code:literal) => { include_bytes!(concat!(env!("OUT_DIR"), "/i18n/", $code, ".tsv.deflate")) @@ -140,6 +140,52 @@ fn inflate(source: &[u8]) -> Result { Ok(text) } +/// Every catalog entry's `contextsource`, stored once for all catalogs (build.rs). +static KEYS: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/i18n/keys.deflate")); + +/// Restore a catalog file from its packed form (build.rs `pack_catalogs`): entry lines refer to +/// [`KEYS`] instead of repeating the English source. Bad data gives an error (and the language +/// falls back to English), never a panic. +fn unpack(source: &[u8]) -> Result { + static KEY_TEXT: OnceLock> = OnceLock::new(); + if source.is_empty() { + return Ok(String::new()); + } + let keys: Vec<&str> = KEY_TEXT.get_or_init(|| inflate(KEYS)).as_ref().map_err(|e| format!("catalog keys: {e}"))?.split('\n').collect(); + expand(&inflate(source)?, &keys) +} + +/// [`unpack`]'s second step: replace each packed entry line with its key and translation. +fn expand(packed: &str, keys: &[&str]) -> Result { + let mut text = String::with_capacity(packed.len().saturating_mul(2)); + let mut next = 0usize; + for (n, line) in packed.split('\n').enumerate() { + if n > 0 { + text.push('\n'); + } + let bad = || format!("packed line {}: bad key reference", n + 1); + let (index, tr) = if let Some(tr) = line.strip_prefix('\x01') { + (next, tr) + } else if let Some(rest) = line.strip_prefix('\x02') { + let (offset, tr) = rest.split_once('\t').ok_or_else(bad)?; + let offset: i64 = offset.parse().map_err(|_| bad())?; + let index = i64::try_from(next).ok().and_then(|n| n.checked_add(offset)).and_then(|i| usize::try_from(i).ok()).ok_or_else(bad)?; + (index, tr) + } else { + text.push_str(line.strip_prefix('\x03').unwrap_or(line)); + continue; + }; + text.push_str(keys.get(index).ok_or_else(bad)?); + text.push('\t'); + text.push_str(tr); + next = index.saturating_add(1); + if text.len() as u64 > MAX_CATALOG_BYTES { + return Err(format!("over {MAX_CATALOG_BYTES} bytes")); + } + } + Ok(text) +} + /// The registry. English first: it is the fallback and the source language. pub static LANGUAGES: [LangInfo; 18] = [ LangInfo { code: "en", name: "English", source: b"", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, @@ -171,7 +217,7 @@ pub static LANGUAGES: [LangInfo; 18] = [ impl LangInfo { /// The catalog file's text (empty for English, or if the bundled data can't be inflated). pub fn text(&self) -> String { - inflate(self.source).unwrap_or_else(|e| { + unpack(self.source).unwrap_or_else(|e| { log::error!("translation catalog {}: {e}; showing English", self.code); String::new() }) @@ -677,13 +723,13 @@ mod tests { assert_eq!(tr(CS(), "All"), "Vše"); } - /// The deflated catalogs in the binary inflate to exactly the `.tsv` files. + /// The packed catalogs in the binary unpack to exactly the `.tsv` files. #[test] - fn deflated_catalogs_match_their_files() { + fn packed_catalogs_match_their_files() { let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/i18n"); for l in LANGUAGES.iter().skip(1) { let file = std::fs::read_to_string(dir.join(format!("{}.tsv", l.code))).unwrap(); - assert_eq!(inflate(l.source).unwrap(), file, "{}", l.code); + assert_eq!(unpack(l.source).unwrap(), file, "{}", l.code); assert!(l.source.len() < file.len() / 2, "{}: stored deflated", l.code); } assert_eq!(LANGUAGES[0].text(), "", "English has no catalog"); @@ -707,6 +753,18 @@ mod tests { assert_eq!(tr(Lang(info), "Layer"), "Layer", "falls back to English"); } + /// Packed lines unpack to their keys; references to keys that don't exist are an error, not a + /// panic. + #[test] + fn packed_lines_unpack_and_bad_references_are_errors() { + let keys = ["\tOpen", "@id\tfile.open", "\tClose"]; + let packed = "# c\n\x01Ouvrir\n\x02-1\tOuvrir\n\x02+1\tFermer\n\x03\x01x\n"; + assert_eq!(expand(packed, &keys).unwrap(), "# c\n\tOpen\tOuvrir\n\tOpen\tOuvrir\n\tClose\tFermer\n\x01x\n"); + for bad in ["\x02-1\tx", "\x02 3\tx", "\x023", "\x029223372036854775807\tx", "\x02-9223372036854775808\tx", "\x01a\n\x01b\n\x01c\n\x01d"] { + assert!(expand(bad, &keys).is_err(), "{bad:?}"); + } + } + /// Every bundled catalog is well-formed and consistent with its sources. #[test] fn bundled_catalogs_are_consistent() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d6309afccc..d9d67542ea 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -70,6 +70,7 @@ pub mod distort_ui; pub mod doc_props_ui; pub mod dock; pub mod duplicate_layer_ui; +pub mod egui_fonts; pub mod enable_rules; pub mod eraser_ui; pub mod export_dialog; diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 0719c5cec1..bee0104b89 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -726,7 +726,8 @@ pub fn install_fonts(ctx: &egui::Context) { /// [`install_fonts`] with the CJK and other-script fallback fonts taken from `cjk` (tests swap the sources). pub fn install_fonts_with(ctx: &egui::Context, cjk: crate::cjk_fonts::Sources) { - let mut fonts = FontDefinitions::default(); + // egui's default fonts stay last in the stack, for symbols and emoji Inter lacks. + let mut fonts = crate::egui_fonts::definitions(); // The bundled fonts are inflated on first use (photocraft_text::fonts). Should one ever fail // to inflate (empty bytes), it is left out and egui's default fonts draw that text. let add = |fonts: &mut FontDefinitions, name: &str, bytes: &'static [u8]| -> Option { diff --git a/packaging/web/README.md b/packaging/web/README.md index 4079c838f6..5ca375f6cc 100644 --- a/packaging/web/README.md +++ b/packaging/web/README.md @@ -7,7 +7,7 @@ static site in `photocraft-web-/`: |---|---| | `index.html` | The page. It loads everything through relative URLs. | | `photocraft-web-.js` | wasm-bindgen glue (generated, ES module) | -| `photocraft-web-_bg.wasm` | The app: about 19 MiB raw, 8 MiB with gzip, 5.6 MiB with Brotli (see [Sizes](#sizes)) | +| `photocraft-web-_bg.wasm` | The app: about 23 MiB raw, 10.5 MiB with gzip (see [Sizes](#sizes)) | | `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | | `fonts/` (optional) | Fonts loaded on demand, listed in `fonts/manifest.txt` (see [Fonts](#fonts)) | @@ -74,19 +74,20 @@ To verify the browser deployment, open the page, open an image and save/export i ## Sizes -Measured on the 0.2.x build (`packaging/web/package.sh`; gzip `-9`, Brotli quality 11): +Measured on 2026-10-12 (0.8.0, `packaging/web/package.sh`; gzip `-9`): -| File | Raw | gzip | Brotli | -|---|---|---|---| -| `photocraft-web-_bg.wasm` | 19,680,726 bytes (18.8 MiB) | 8,177,260 (7.8 MiB) | 5,868,731 (5.6 MiB) | -| `photocraft-web-.js` | about 160 KB | about 23 KB | about 20 KB | +| File | Raw | gzip | +|---|---|---| +| `photocraft-web-_bg.wasm` | 24,167,186 bytes (23.0 MiB) | 10,998,053 (10.5 MiB) | +| `photocraft-web-.js` | about 170 KB | about 26 KB | Per-file upload limits: Cloudflare Pages and Workers static assets reject any file over **25 MiB** (26,214,400 bytes). The wasm fits, and `packaging/web/package.sh` fails the build if it ever grows past 24 MiB, so a release can't ship a file a common host refuses. (Releases up to 0.2.0 shipped a 25.8 MiB wasm, which Cloudflare rejected; issue #198.) The size comes from the -`wasm-release` Cargo profile (fat LTO, size-optimized code with the pixel crates kept at full -speed) plus `wasm-opt -Oz`. +`wasm-release` Cargo profile (fat LTO, size-optimized code with the pixel and file-decoding +crates kept at speed) plus `wasm-opt -Oz`, and from storing the bundled fonts (PhotoCraft's and +egui's) and the translation catalogs compressed, unpacked on first use. ## Any path works diff --git a/xtask/src/i18n_coverage.rs b/xtask/src/i18n_coverage.rs index 105f927435..86a49151d4 100644 --- a/xtask/src/i18n_coverage.rs +++ b/xtask/src/i18n_coverage.rs @@ -94,7 +94,7 @@ fn registered_languages(source: &str) -> Result, String> { let catalog = if code == "en" { None } else { - // `catalog!("xx")` embeds `xx.tsv` (deflated by ui-egui's build.rs). + // `catalog!("xx")` embeds `xx.tsv` (packed by ui-egui's build.rs). let marker = "catalog!(\""; let start = block.find(marker).ok_or_else(|| format!("language `{code}` has no catalog! entry"))? + marker.len(); let rest = block.get(start..).ok_or_else(|| format!("language `{code}` has a malformed catalog path"))?; From fd21e734111ef0af3f26b77e3e59d3d651c50f91 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Mon, 12 Oct 2026 16:06:16 +0200 Subject: [PATCH 12/27] Format: check pattern sizes on load (#3478) (#3484) * Format: check pattern sizes on load (#3478) A .pcraft pattern's width/height came straight from the manifest, so a 1 KB file could declare a 65535x65535 pattern without tiles; painting it allocated width*height*16 bytes (abort) and the engine's u32 size math overflowed. - format: reject patterns 0 px on a side (Corrupt) or above the PSD reader's 30 000 px edge (LimitExceeded). - doc: Pattern::pixel_count / is_paintable with MAX_PAINT_PIXELS (2^26, a 1 GiB f32 RGBA working copy). - compose/gpu: patterns too large to paint paint nothing instead of allocating (Tile::new_in, pattern_rgba, PreparedPatterns, GPU plan). - engine: brush.texturePattern refuses them; average_color uses u64 math and a bounded corner. Co-Authored-By: Claude Opus 5.5 * gpu: skip unpaintable patterns in the Bevel texture too The Bevel & Emboss texture element uploaded the pattern through pattern_rgba, which now returns no pixels for a pattern too large to paint; filter it like the other GPU pattern lookups. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/compose/src/pattern.rs | 24 +++++++++++++++--- crates/doc/src/pattern.rs | 17 +++++++++++++ crates/engine/src/fill_cmds.rs | 2 +- crates/engine/src/paint_cmds.rs | 2 +- crates/engine/src/pattern_cmds.rs | 23 ++++++++++++++--- .../engine/src/retouch_cmds/pattern_stamp.rs | 2 +- crates/format/src/convert.rs | 9 +++++++ crates/format/tests/corruption.rs | 25 +++++++++++++++++++ crates/gpu/src/fx.rs | 2 +- crates/gpu/src/plan.rs | 6 ++--- 10 files changed, 98 insertions(+), 14 deletions(-) diff --git a/crates/compose/src/pattern.rs b/crates/compose/src/pattern.rs index a55c2c8abc..a2c5c52fb9 100644 --- a/crates/compose/src/pattern.rs +++ b/crates/compose/src/pattern.rs @@ -45,7 +45,7 @@ impl<'a> PreparedPatterns<'a> { pub(crate) fn get(&self, id: &str, name: &str) -> Option> { let p = photocraft_doc::pattern::find(self.patterns, id, name)?; - if p.is_empty() { + if !p.is_paintable() { return None; } let bytes = usize::try_from(p.width) @@ -87,9 +87,13 @@ impl<'a> PreparedPatterns<'a> { /// colour mode `mode`. A pattern keeps its own colours (an RGB pattern stays RGB, in the /// library and in the document), but in a Grayscale or CMYK document it paints that mode's /// colours: what Edit › Fill writes into the document's pixels, and what Photoshop shows (the -/// pattern in colour in its picker, filled in grey). +/// pattern in colour in its picker, filled in grey). Empty for a pattern that isn't +/// [paintable](Pattern::is_paintable). pub fn pattern_rgba(p: &Pattern, mode: ColorMode) -> Vec<[f32; 4]> { - let mut px = vec![[0.0f32; 4]; (p.width as usize).saturating_mul(p.height as usize)]; + let Some(n) = p.is_paintable().then(|| usize::try_from(p.pixel_count()).ok()).flatten() else { + return Vec::new(); + }; + let mut px = vec![[0.0f32; 4]; n]; p.surface.read_rgba_into(p.rect(), &mut px); let paints_in = matches!(mode, ColorMode::Grayscale | ColorMode::Bitmap | ColorMode::Duotone | ColorMode::Cmyk); if paints_in && p.surface.format().mode != mode { @@ -117,8 +121,9 @@ impl Tile { } /// The tile of `p` painted into a document of colour mode `mode` (see [`pattern_rgba`]). + /// `None` when the pattern isn't [paintable](Pattern::is_paintable). pub fn new_in(p: &Pattern, mode: ColorMode) -> Option { - if p.is_empty() { + if !p.is_paintable() { return None; } let (w, h) = (p.width as usize, p.height as usize); @@ -255,6 +260,17 @@ mod tests { assert_eq!(px[8], [0.0, 0.0, 1.0, 1.0]); // x = 4 } + #[test] + fn patterns_too_large_to_paint_are_skipped_without_allocating() { + // Only the size is declared (no tiles): a hostile file can claim any size. + let p = Pattern { id: "big".into(), name: "Big".into(), width: 9000, height: 8000, surface: Surface::new(PixelFormat::RGBA8) }; + assert!(!p.is_paintable()); + assert!(Tile::new(&p).is_none()); + assert!(pattern_rgba(&p, ColorMode::Rgb).is_empty()); + let list = [p]; + assert!(PreparedPatterns::new(&list, 0).get("big", "").is_none()); + } + #[test] fn link_phase_scale_and_angle_move_the_origin() { let t = Tile::new(&checker()).unwrap(); diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index 47279e2c86..9a43c0a467 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -5,6 +5,13 @@ use photocraft_geom::Rect; use photocraft_raster::Surface; +/// Largest pattern edge a file may declare (the PSD reader's limit; Define Pattern stops at 4000). +pub const MAX_EDGE: u32 = 30_000; + +/// Most pixels a pattern may have to be painted. Renderers expand a pattern into a 16-byte-per-pixel +/// working copy, so this keeps that copy at 1 GiB; larger patterns paint nothing. +pub const MAX_PAINT_PIXELS: u64 = 1 << 26; + /// One pattern tile. Pixels cover `0..width × 0..height` of `surface`, in any pixel format /// (depth and colour model are the pattern's own, as in Photoshop). #[derive(Clone, Debug, PartialEq)] @@ -57,6 +64,16 @@ impl Pattern { pub fn is_empty(&self) -> bool { self.width == 0 || self.height == 0 } + + /// Pixels in the tile (`width × height`, never overflowing). + pub fn pixel_count(&self) -> u64 { + u64::from(self.width) * u64::from(self.height) + } + + /// Whether the tile has pixels and is small enough to paint (see [`MAX_PAINT_PIXELS`]). + pub fn is_paintable(&self) -> bool { + !self.is_empty() && self.pixel_count() <= MAX_PAINT_PIXELS + } } /// Display form of a stored pattern name (see [`Pattern::display_name`]). diff --git a/crates/engine/src/fill_cmds.rs b/crates/engine/src/fill_cmds.rs index a7350dafc1..130977ce28 100644 --- a/crates/engine/src/fill_cmds.rs +++ b/crates/engine/src/fill_cmds.rs @@ -106,7 +106,7 @@ pub fn fill(s: &mut Session, p: &Value) -> Result { "pattern" => { let pat = crate::pattern_cmds::resolve_param(s, CMD, p)?; let (scale, angle, _, phase) = crate::pattern_cmds::placement(p); - let tile = photocraft_compose::pattern::Tile::new(&pat).ok_or_else(|| bad("the pattern is empty"))?; + let tile = photocraft_compose::pattern::Tile::new(&pat).ok_or_else(|| bad("the pattern is empty or too large to paint"))?; // Photoshop's Fill uses the canvas origin. let place = photocraft_compose::pattern::Placement::new(Rect::EMPTY, false, phase, scale, angle); out["pattern"] = json!(pat.id); diff --git a/crates/engine/src/paint_cmds.rs b/crates/engine/src/paint_cmds.rs index 7366758e93..ad16cd85d9 100644 --- a/crates/engine/src/paint_cmds.rs +++ b/crates/engine/src/paint_cmds.rs @@ -55,7 +55,7 @@ fn bucket(s: &mut Session, p: &Value) -> Result { let pattern = if source == "pattern" { let pat = crate::pattern_cmds::resolve_param(s, "paint.bucket", p)?; let tile = photocraft_compose::pattern::Tile::new(&pat) - .ok_or_else(|| EngineError::BadParams { cmd: "paint.bucket".into(), msg: "the pattern is empty".into() })?; + .ok_or_else(|| EngineError::BadParams { cmd: "paint.bucket".into(), msg: "the pattern is empty or too large to paint".into() })?; let (scale, angle, _, phase) = crate::pattern_cmds::placement(p); Some((tile, scale, angle, phase)) } else { diff --git a/crates/engine/src/pattern_cmds.rs b/crates/engine/src/pattern_cmds.rs index 21bee1283c..d58fd9e827 100644 --- a/crates/engine/src/pattern_cmds.rs +++ b/crates/engine/src/pattern_cmds.rs @@ -326,8 +326,12 @@ fn new_fill_layer(s: &mut Session, p: &Value) -> Result { fn brush_texture(s: &mut Session, p: &Value) -> Result { let pat = resolve_param(s, "brush.texturePattern", p)?; + let n = pat.is_paintable().then(|| usize::try_from(pat.pixel_count()).ok()).flatten(); + let Some(n) = n else { + return Err(bad("brush.texturePattern", "the pattern is empty or too large to paint")); + }; // Brush textures are grey: luminance of the composite over white. - let mut px = vec![[0.0f32; 4]; (pat.width * pat.height) as usize]; + let mut px = vec![[0.0f32; 4]; n]; pat.surface.read_rgba_into(pat.rect(), &mut px); let lum: Vec = px .iter() @@ -348,8 +352,11 @@ fn brush_texture(s: &mut Session, p: &Value) -> Result { /// A swatch colour for UIs (average of the tile), straight RGB. pub fn average_color(p: &Pattern) -> Color { - let mut px = vec![[0.0f32; 4]; (p.width * p.height) as usize]; - p.surface.read_rgba_into(p.rect(), &mut px); + // A pattern too large to paint is averaged over its paintable top-left corner. + let side = photocraft_doc::pattern::MAX_PAINT_PIXELS.isqrt() as u32; + let (w, h) = (p.width.min(side), p.height.min(side)); + let mut px = vec![[0.0f32; 4]; w as usize * h as usize]; + p.surface.read_rgba_into(Rect::new(0, 0, w as i32, h as i32), &mut px); let n = px.len().max(1) as f32; let s = px.iter().fold([0.0f32; 3], |a, q| [a[0] + q[0], a[1] + q[1], a[2] + q[2]]); Color::rgb(s[0] / n, s[1] / n, s[2] / n) @@ -560,4 +567,14 @@ mod tests { assert!(s.tools.brush.texture.enabled); assert!(matches!(s.tools.brush.texture.pattern, photocraft_paint::Pattern::Tile(_))); } + + #[test] + fn a_pattern_too_large_to_paint_is_refused_not_allocated() { + // width × height overflows u32: this panicked (debug) or wrapped (release). + let mut s = session(8); + let huge = Pattern { id: "huge".into(), name: "Huge".into(), width: 65_536, height: 65_536, surface: Surface::new(PixelFormat::RGBA8) }; + s.patterns.items.push(huge); + let e = s.execute("brush.texturePattern", json!({"pattern": "huge"})).unwrap_err(); + assert!(e.to_string().contains("too large"), "{e}"); + } } diff --git a/crates/engine/src/retouch_cmds/pattern_stamp.rs b/crates/engine/src/retouch_cmds/pattern_stamp.rs index cb9e0adfe9..0dc9f102ac 100644 --- a/crates/engine/src/retouch_cmds/pattern_stamp.rs +++ b/crates/engine/src/retouch_cmds/pattern_stamp.rs @@ -45,7 +45,7 @@ pub(super) fn run(s: &mut Session, p: &Value) -> Result { return Err(bad(CMD, "`points` is empty")); }; let phase = stamp_phase(p, CMD, aligned, first)?; - let tile = Tile::new(&pat).ok_or_else(|| bad(CMD, "the pattern is empty"))?; + let tile = Tile::new(&pat).ok_or_else(|| bad(CMD, "the pattern is empty or too large to paint"))?; reject_huge_stroke(CMD, &stroke)?; let seed = p.get("seed").and_then(Value::as_u64).unwrap_or_else(|| { diff --git a/crates/format/src/convert.rs b/crates/format/src/convert.rs index 4dbbb6aa38..47ea5383d7 100644 --- a/crates/format/src/convert.rs +++ b/crates/format/src/convert.rs @@ -592,6 +592,15 @@ impl Loader<'_> { } let mut patterns = Vec::with_capacity(m.patterns.len()); for p in &m.patterns { + // Sizes come from the file: a pattern with no tiles can claim any size, and renderers + // allocate per pixel. + let max = photocraft_doc::pattern::MAX_EDGE; + if p.width == 0 || p.height == 0 { + return Err(FormatError::corrupt(format!("pattern `{}` is {}×{} px", p.name, p.width, p.height))); + } + if p.width > max || p.height > max { + return Err(FormatError::LimitExceeded(format!("pattern `{}` is {}×{} px (max {max} per side)", p.name, p.width, p.height))); + } patterns.push(Pattern { id: p.id.clone(), name: p.name.clone(), width: p.width, height: p.height, surface: self.surface(&p.surface)? }); } let layer_comps = m.layer_comps.iter().map(|c| self.comp(c)).collect::>>()?; diff --git a/crates/format/tests/corruption.rs b/crates/format/tests/corruption.rs index 2f47203b3f..2ef07bcd45 100644 --- a/crates/format/tests/corruption.rs +++ b/crates/format/tests/corruption.rs @@ -419,6 +419,31 @@ fn tile_coordinates_outside_the_i32_pixel_range_are_rejected() { } } +/// Rewrites the first pattern's declared size (its tiles stay as saved). +fn with_pattern_size(w: u64, h: u64) -> Vec { + rebuild(sample_cached(), |n, d| { + if n != "manifest.json" { + return Some(d); + } + let mut v: serde_json::Value = serde_json::from_slice(&d).unwrap(); + let p = &mut v["document"]["patterns"][0]; + assert!(p.is_object(), "the sample has a pattern"); + p["width"] = w.into(); + p["height"] = h.into(); + Some(serde_json::to_vec(&v).unwrap()) + }) +} + +#[test] +fn pattern_sizes_from_the_file_are_checked() { + // A pattern can claim any size without tiles; painting it allocated per pixel (abort). + let huge = load_from_bytes(&with_pattern_size(65_535, 65_535)); + assert!(matches!(huge, Err(FormatError::LimitExceeded(_))), "{huge:?}"); + let empty = load_from_bytes(&with_pattern_size(0, 24)); + assert!(matches!(empty, Err(FormatError::Corrupt(_))), "{empty:?}"); + assert!(load_from_bytes(&with_pattern_size(32, 24)).is_ok()); +} + /// A canvas is a few bytes of manifest JSON; one past the budget is refused before any work (#2935). /// The default budget is the largest canvas the app creates (300000 × 300000, as `file.new`, /// Canvas Size and PSB import allow), so everything a user can save reopens. diff --git a/crates/gpu/src/fx.rs b/crates/gpu/src/fx.rs index 9eba5888b8..cc396d8ba0 100644 --- a/crates/gpu/src/fx.rs +++ b/crates/gpu/src/fx.rs @@ -389,7 +389,7 @@ pub(crate) fn program_with(e: &Effect, light: &GlobalLight, vector_shape: bool, h = b.push(st); } if let Some(t) = &bv.texture - && let Some(pat) = photocraft_doc::pattern::find(patterns, &t.id, &t.name).filter(|p| !p.is_empty()) + && let Some(pat) = photocraft_doc::pattern::find(patterns, &t.id, &t.name).filter(|p| p.is_paintable()) { // Texture element (`effects::bevel_height`): luminance × depth / unit added. let unit = if bv.depth.abs() > 1e-6 { (g.depth / bv.depth).abs().max(1e-3) } else { g.width.max(1.0) }; diff --git a/crates/gpu/src/plan.rs b/crates/gpu/src/plan.rs index f5a3b573e4..fb135dc135 100644 --- a/crates/gpu/src/plan.rs +++ b/crates/gpu/src/plan.rs @@ -803,7 +803,7 @@ impl<'a> Planner<'a> { // compose::render_fill: the pattern tiled from the layer's frame (transparent // when missing), then the layer's masks. let empty = self.clear(); - let Some(pat) = photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| !p.is_empty()) else { return Ok(empty) }; + let Some(pat) = photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| p.is_paintable()) else { return Ok(empty) }; let frame = photocraft_compose::fill_frame(layer, self.cx.canvas); let anchor = if frame.is_empty() { (0.0, 0.0) } else { (f64::from(frame.x0), f64::from(frame.y0)) }; let paint = Paint::Pattern(pat, placement(anchor, *link, *phase, *scale, *angle)); @@ -1097,7 +1097,7 @@ impl<'a> Planner<'a> { }; for &(_, e) in &rev { if let Effect::PatternOverlay { common, name, id, scale, angle, link, phase } = e - && let Some(pat) = photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| !p.is_empty()) + && let Some(pat) = photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| p.is_paintable()) { let paint = Paint::Pattern(pat, placement(anchor, *link, *phase, *scale, *angle)); l = self.paint(l, content, Cov::One, &paint, common.blend, common.opacity, F_GATE, clip, sb); @@ -1312,7 +1312,7 @@ impl<'a> Planner<'a> { match p { FxPaint::Color(c) => Paint::Color(c.to_rgb()), FxPaint::Gradient(g) => Paint::Gradient(g), - FxPaint::Pattern { name, id, scale } => match photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| !p.is_empty()) { + FxPaint::Pattern { name, id, scale } => match photocraft_doc::pattern::find(self.cx.patterns, id, name).filter(|p| p.is_paintable()) { Some(pat) => Paint::Pattern(pat, placement(anchor, true, (0.0, 0.0), *scale, 0.0)), None => Paint::None, }, From 73a56d76032a9d94cda7c838d110f82c3914d8be Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Mon, 12 Oct 2026 16:08:20 +0200 Subject: [PATCH 13/27] Format: overflow-safe blob decode and bounds-checked ZIP reads (#3486) (#3501) - store: `max_blob_bytes as u64 + 1` overflowed for usize::MAX (debug panic; release take(0), so every blob failed). The blob read and the tile decode now share decompress_bounded(), which saturates. - zip: ZipReader::read sliced `data_offset..data_offset + compressed` unchecked although Entry is pub + Clone; a foreign entry is now a Corrupt error. - store: Collect::tile reuses tile_le(); Object::content() gives the stored bytes once (existing_object_is_valid no longer builds them twice). Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/format/src/store.rs | 65 +++++++++++++++---------------- crates/format/src/zip.rs | 20 +++++++++- crates/format/tests/corruption.rs | 9 +++++ 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/crates/format/src/store.rs b/crates/format/src/store.rs index 674c99f214..c14b488250 100644 --- a/crates/format/src/store.rs +++ b/crates/format/src/store.rs @@ -30,14 +30,24 @@ fn compress(data: &[u8]) -> Vec { ruzstd::encoding::compress_to_vec(data, ruzstd::encoding::CompressionLevel::Fastest) } -fn decompress(data: &[u8], expected: usize, what: &str) -> Result> { - let mut dec = ruzstd::decoding::StreamingDecoder::new(data).map_err(|e| FormatError::corrupt(format!("{what}: zstd: {e}")))?; - let mut out = Vec::with_capacity(expected.min(64 << 20)); - (&mut dec).take(expected as u64 + 1).read_to_end(&mut out).map_err(|e| FormatError::corrupt(format!("{what}: zstd: {e}")))?; - if out.len() != expected { - return Err(FormatError::corrupt(format!("{what}: expected {expected} bytes, got {}", out.len()))); +/// Decodes a zstd frame of at most `max` bytes (`LimitExceeded` beyond). With `exact`, the +/// decoded length must be exactly that (`Corrupt` otherwise). +fn decompress_bounded(data: &[u8], max: usize, exact: Option, what: &str) -> Result> { + let zstd = |e: &dyn std::fmt::Display| FormatError::corrupt(format!("{what}: zstd: {e}")); + let mut dec = ruzstd::decoding::StreamingDecoder::new(data).map_err(|e| zstd(&e))?; + let mut out = Vec::with_capacity(exact.unwrap_or(0).min(max).min(64 << 20)); + // `max` may be `usize::MAX`: one byte past it must not overflow. + let limit = u64::try_from(max).unwrap_or(u64::MAX).saturating_add(1); + (&mut dec).take(limit).read_to_end(&mut out).map_err(|e| zstd(&e))?; + match exact { + Some(n) if out.len() != n => Err(FormatError::corrupt(format!("{what}: expected {n} bytes, got {}", out.len()))), + None if out.len() > max => Err(FormatError::LimitExceeded(format!("{what} exceeds {max} bytes"))), + _ => Ok(out), } - Ok(out) +} + +fn decompress(data: &[u8], expected: usize, what: &str) -> Result> { + decompress_bounded(data, expected, Some(expected), what) } fn hash_bytes(b: &[u8]) -> Hash { @@ -228,12 +238,7 @@ impl Fetch for LoadFetch<'_> { let z = self.src.get(&path, self.opts.max_blob_bytes)?; // Blob length is not in the manifest; the frame header bounds it and // we cap at max_blob_bytes. - let mut dec = ruzstd::decoding::StreamingDecoder::new(&z[..]).map_err(|e| FormatError::corrupt(format!("{path}: zstd: {e}")))?; - let mut data = Vec::new(); - (&mut dec).take(self.opts.max_blob_bytes as u64 + 1).read_to_end(&mut data).map_err(|e| FormatError::corrupt(format!("{path}: zstd: {e}")))?; - if data.len() > self.opts.max_blob_bytes { - return Err(FormatError::LimitExceeded(format!("{path} exceeds max_blob_bytes"))); - } + let data = decompress_bounded(&z, self.opts.max_blob_bytes, None, &path)?; self.account(data.len())?; if hash_bytes(&data) != hash { return Err(FormatError::corrupt(format!("{path}: content does not match its hash"))); @@ -313,13 +318,7 @@ impl Sink for Collect<'_> { self.tiles.entry(h.clone()).or_insert_with(|| (tile.clone(), format.sample)); return h; } - let h = if cfg!(target_endian = "big") { - let mut b = tile.bytes().to_vec(); - swap_to_le(&mut b, format.sample); - hash_bytes(&b) - } else { - hash_bytes(tile.bytes()) - }; + let h = hash_bytes(&tile_le(tile, format.sample)); if let Some(cache) = self.hash_cache.as_deref_mut() { cache.insert(key, (Arc::downgrade(tile), h.clone())); } @@ -414,21 +413,23 @@ enum Object { impl Object { fn compressed(&self) -> Vec { - match self { - Object::Tile(t, s) => compress(&tile_le(t, *s)), - Object::Blob(b) => compress(b), - } + compress(&self.content()) } - fn matches_content_hash(&self, compressed: &[u8], what: &str) -> bool { - let expected = match self { + /// The bytes this object stores (tiles little-endian). + fn content(&self) -> std::borrow::Cow<'_, [u8]> { + match self { Object::Tile(tile, sample) => tile_le(tile, *sample), Object::Blob(blob) => std::borrow::Cow::Borrowed(blob.as_slice()), - }; - decompress(compressed, expected.len(), what).is_ok_and(|actual| hash_bytes(&actual) == hash_bytes(&expected)) + } } } +/// Whether `compressed` decodes to exactly `expected`. +fn content_matches(expected: &[u8], compressed: &[u8], what: &str) -> bool { + decompress(compressed, expected.len(), what).is_ok_and(|actual| hash_bytes(&actual) == hash_bytes(expected)) +} + fn file_signature(metadata: &std::fs::Metadata) -> Option { #[cfg(unix)] let unix = { @@ -587,10 +588,8 @@ fn existing_object_is_valid(path: &Path, object: &Object, what: &str) -> Result< } let signature = file_signature(&metadata); - let expected_len = match object { - Object::Tile(tile, sample) => tile_le(tile, *sample).len(), - Object::Blob(blob) => blob.len(), - }; + let expected = object.content(); + let expected_len = expected.len(); // This is the same bound used by the tile loader. It comfortably includes // zstd framing overhead while keeping damaged files bounded when inspected. let max_compressed_len = expected_len.saturating_add(expected_len / 8).saturating_add(4096); @@ -604,7 +603,7 @@ fn existing_object_is_valid(path: &Path, object: &Object, what: &str) -> Result< if compressed.len() > max_compressed_len { return Ok((false, None)); } - Ok((object.matches_content_hash(&compressed, what), signature)) + Ok((content_matches(&expected, &compressed, what), signature)) } /// A bundle path and its compressed bytes. diff --git a/crates/format/src/zip.rs b/crates/format/src/zip.rs index 1e296e16d1..9ec92c2fba 100644 --- a/crates/format/src/zip.rs +++ b/crates/format/src/zip.rs @@ -201,7 +201,12 @@ impl<'a> ZipReader<'a> { if e.uncompressed > max { return Err(FormatError::LimitExceeded(format!("zip entry `{}` is {} bytes (max {max})", e.name, e.uncompressed))); } - let raw = &self.bytes[e.data_offset..e.data_offset + e.compressed]; + // `Entry` is public and cloneable: one from another archive must not index out of range. + let raw = e + .data_offset + .checked_add(e.compressed) + .and_then(|end| self.bytes.get(e.data_offset..end)) + .ok_or_else(|| FormatError::corrupt(format!("zip entry `{}` out of range", e.name)))?; let data = match e.method { 0 => raw.to_vec(), 8 => { @@ -366,6 +371,19 @@ mod tests { assert!(matches!(archive.read_by_name("manifest.json", usize::MAX), Err(FormatError::Corrupt(_)))); } + #[test] + fn an_entry_from_another_archive_is_an_error_not_a_panic() { + let mut big = ZipWriter::new(); + big.add("a", &[7; 4096]).unwrap(); + let big = big.finish().unwrap(); + let foreign = ZipReader::new(&big).unwrap().entries[0].clone(); + let mut small = ZipWriter::new(); + small.add("a", b"x").unwrap(); + let small = small.finish().unwrap(); + let r = ZipReader::new(&small).unwrap(); + assert!(matches!(r.read(&foreign, usize::MAX), Err(FormatError::Corrupt(_)))); + } + #[test] fn offset_access_rejects_overflow() { assert!(u16_at(&[], usize::MAX).is_err()); diff --git a/crates/format/tests/corruption.rs b/crates/format/tests/corruption.rs index 2ef07bcd45..d293abed1e 100644 --- a/crates/format/tests/corruption.rs +++ b/crates/format/tests/corruption.rs @@ -419,6 +419,15 @@ fn tile_coordinates_outside_the_i32_pixel_range_are_rejected() { } } +#[test] +fn an_unlimited_blob_budget_still_loads_blobs() { + // `max_blob_bytes + 1` overflowed (debug panic; release read zero bytes, failing every blob). + let doc = rich_doc(ColorMode::Rgb, SampleType::U8); + let bytes = save_to_bytes(&doc, &SaveOptions::default()).unwrap(); + let opts = LoadOptions { max_blob_bytes: usize::MAX, ..LoadOptions::default() }; + assert_eq!(load_from_bytes_with(&bytes, &opts).unwrap(), doc); +} + /// Rewrites the first pattern's declared size (its tiles stay as saved). fn with_pattern_size(w: u64, h: u64) -> Vec { rebuild(sample_cached(), |n, d| { From 423421a879858fbfe46ac701a082ba706055c5f3 Mon Sep 17 00:00:00 2001 From: null 00000111 <287216888+s17sh70x2jfl031ag@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:19:06 -0700 Subject: [PATCH 14/27] Fuzz: ignore the same build leftovers in every fuzz crate (#3585) crates/raw/fuzz had no .gitignore, crates/psd/fuzz didn't ignore Cargo.lock and crates/format/fuzz didn't ignore coverage/, so running those fuzz targets left untracked files in the checkout. All five fuzz crates now ignore target/, corpus/, artifacts/, coverage/ and Cargo.lock, as codecs and affinity already did. --- crates/format/fuzz/.gitignore | 1 + crates/psd/fuzz/.gitignore | 1 + crates/raw/fuzz/.gitignore | 5 +++++ 3 files changed, 7 insertions(+) create mode 100644 crates/raw/fuzz/.gitignore diff --git a/crates/format/fuzz/.gitignore b/crates/format/fuzz/.gitignore index d932d310fa..fc87536ae7 100644 --- a/crates/format/fuzz/.gitignore +++ b/crates/format/fuzz/.gitignore @@ -2,3 +2,4 @@ target/ corpus/ artifacts/ Cargo.lock +coverage/ diff --git a/crates/psd/fuzz/.gitignore b/crates/psd/fuzz/.gitignore index 1a45eee776..5c404b9583 100644 --- a/crates/psd/fuzz/.gitignore +++ b/crates/psd/fuzz/.gitignore @@ -2,3 +2,4 @@ target corpus artifacts coverage +Cargo.lock diff --git a/crates/raw/fuzz/.gitignore b/crates/raw/fuzz/.gitignore new file mode 100644 index 0000000000..ab0eaa1a49 --- /dev/null +++ b/crates/raw/fuzz/.gitignore @@ -0,0 +1,5 @@ +target/ +corpus/ +artifacts/ +coverage/ +Cargo.lock From 0b935da8822f5294c22548f9facacf4c3fcd06e3 Mon Sep 17 00:00:00 2001 From: null 00000111 <287216888+s17sh70x2jfl031ag@users.noreply.github.com> Date: Mon, 12 Oct 2026 07:22:11 -0700 Subject: [PATCH 15/27] HEIF: move the heic-rs pin from 0.1.1 to 0.1.4 (#3637) heic-rs 0.1.2 to 0.1.4 fix six panics on malformed files, found by fuzzing: four in release builds (a uuid box too small for its extended type, a block starting right of the picture, a conformance window larger than the picture, and a reference row below the picture) and two with overflow checks on (a PCM bit depth above the sample bit depth, a slice QP out of range). The API, MSRV and output are unchanged. Both pins move together: crates/heif and the codecs fuzz target, which calls heic-rs directly. guarded() stays as the backstop it was meant to be. --- Cargo.lock | 4 ++-- crates/codecs/fuzz/Cargo.toml | 2 +- crates/heif/Cargo.toml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 160403c482..a3948361bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2331,9 +2331,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "heic-rs" -version = "0.1.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf9dde634bb6d5b718bacd01bef80192f2334306b2f785519a0136cca7b9618" +checksum = "8883c3bfa6b4ff19a40e52a6dc051c5612cbe21bf408fc6d4875e8dfd777cb66" dependencies = [ "rayon", ] diff --git a/crates/codecs/fuzz/Cargo.toml b/crates/codecs/fuzz/Cargo.toml index 9cbb3d9815..62ec96c523 100644 --- a/crates/codecs/fuzz/Cargo.toml +++ b/crates/codecs/fuzz/Cargo.toml @@ -16,7 +16,7 @@ cargo-fuzz = true libfuzzer-sys = "0.4" photocraft-codecs = { path = "..", features = ["heif"] } # Called directly too: the codec turns its panics into errors (see decode_heif.rs). -heic-rs = "=0.1.1" +heic-rs = "=0.1.4" [profile.release] debug = 1 diff --git a/crates/heif/Cargo.toml b/crates/heif/Cargo.toml index f8ddf6457a..18210b0c46 100644 --- a/crates/heif/Cargo.toml +++ b/crates/heif/Cargo.toml @@ -9,11 +9,11 @@ rust-version.workspace = true [dependencies] # Pure Rust, no unsafe. Pinned exactly: a young, single-owner crate that parses untrusted files; # moving the pin is a reviewed change (re-run the fuzz target and the heif corpus tests). -heic-rs = { version = "=0.1.1", default-features = false, features = ["std"] } +heic-rs = { version = "=0.1.4", default-features = false, features = ["std"] } # The tiles of a HEIC photo decode in parallel on native; wasm stays single-threaded. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -heic-rs = { version = "=0.1.1", default-features = false, features = ["std", "parallel"] } +heic-rs = { version = "=0.1.4", default-features = false, features = ["std", "parallel"] } [lints] workspace = true From 019a391f40728f94dc8e1ce8f3bda65bead81fcf Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Mon, 12 Oct 2026 16:33:21 +0200 Subject: [PATCH 16/27] Codecs: decode only the requested EXR part; Cryptomatte uses the cumulative cap (#3721) (#3722) read_layer decoded every part and picked one; use a first-valid-layer reader that declines all other parts. Cryptomatte now goes through read_meta and read_layer. Enable exr/rayon on native targets. Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 1 + crates/codecs/Cargo.toml | 2 ++ crates/codecs/src/codecs/exr.rs | 33 ++++++++++++++++----- crates/codecs/src/codecs/exr_cryptomatte.rs | 12 ++------ crates/codecs/tests/exr_multipart.rs | 29 ++++++++++++++++++ 5 files changed, 61 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3948361bb..f17edd1171 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1743,6 +1743,7 @@ dependencies = [ "miniz_oxide 0.8.9", "num-complex", "pulp", + "rayon-core", "smallvec", "zune-inflate", ] diff --git a/crates/codecs/Cargo.toml b/crates/codecs/Cargo.toml index a55dcf1213..c462d0a39b 100644 --- a/crates/codecs/Cargo.toml +++ b/crates/codecs/Cargo.toml @@ -36,6 +36,8 @@ photocraft-heif = { path = "../heif", optional = true } # Orientation is applied a band of rows per task on native; wasm stays single-threaded. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rayon.workspace = true +# EXR block (de)compression runs on the rayon pool natively; wasm stays single-threaded. +exr = { version = "1.73", default-features = false, features = ["rayon"] } [dev-dependencies] proptest.workspace = true diff --git a/crates/codecs/src/codecs/exr.rs b/crates/codecs/src/codecs/exr.rs index a4014db0b3..2506dd062b 100644 --- a/crates/codecs/src/codecs/exr.rs +++ b/crates/codecs/src/codecs/exr.rs @@ -35,7 +35,7 @@ fn map_exr(e: exr::error::Error) -> CodecError { /// Read the headers without the `exr` crate's validation (it rejects deep parts outright), /// after checking every part's declared size against the limits, before any pixel allocation. -fn read_meta(bytes: &[u8], limits: &Limits) -> Result { +pub(super) fn read_meta(bytes: &[u8], limits: &Limits) -> Result { let meta = MetaData::read_from_buffered(Cursor::new(bytes), false).map_err(err)?; // Flat decoding reads every part at once, so the parts' sizes are also capped together. let mut total: u64 = 0; @@ -93,12 +93,31 @@ fn pick_part(meta: &MetaData) -> usize { best } -/// Decode the flat layers via the `exr` crate (it handles every flat compression) and -/// return the requested part's layer. -fn read_layer(bytes: &[u8], index: usize) -> Result>, CodecError> { - let image = - read().no_deep_data().largest_resolution_level().all_channels().all_layers().all_attributes().from_buffered(Cursor::new(bytes)).map_err(map_exr)?; - image.layer_data.into_iter().nth(index).ok_or_else(|| err(format!("part {index} is missing from the file"))) +/// Selects one part for the `exr` crate's first-valid-layer reader: `create_channels_reader` +/// is asked about the headers in file order and the first `Ok` wins, so every header before +/// `index` (and, harmlessly, those after it) is declined. Only the chosen part's chunks are +/// then decompressed and only its planes allocated; the other parts are skipped unread. +struct OnlyPart { + inner: C, + index: usize, + seen: std::cell::Cell, +} + +impl<'s, C: ReadChannels<'s>> ReadChannels<'s> for OnlyPart { + type Reader = C::Reader; + + fn create_channels_reader(&'s self, header: &exr::meta::header::Header) -> exr::error::Result { + let position = self.seen.get(); + self.seen.set(position.saturating_add(1)); + if position == self.index { self.inner.create_channels_reader(header) } else { Err(exr::error::Error::Invalid("part not requested".into())) } + } +} + +/// Decode one part's flat layer via the `exr` crate (it handles every flat compression). +/// Other parts are not decompressed, so an AOV file costs one part per request, not the file. +pub(super) fn read_layer(bytes: &[u8], index: usize) -> Result>, CodecError> { + let only = OnlyPart { inner: read().no_deep_data().largest_resolution_level().all_channels(), index, seen: std::cell::Cell::new(0) }; + only.first_valid_layer().all_attributes().from_buffered(Cursor::new(bytes)).map(|image| image.layer_data).map_err(map_exr) } fn layer_to_image(layer: &Layer>) -> Result { diff --git a/crates/codecs/src/codecs/exr_cryptomatte.rs b/crates/codecs/src/codecs/exr_cryptomatte.rs index f28c57ab47..2fabd0c6c5 100644 --- a/crates/codecs/src/codecs/exr_cryptomatte.rs +++ b/crates/codecs/src/codecs/exr_cryptomatte.rs @@ -11,7 +11,6 @@ use std::io::Cursor; use exr::meta::MetaData; use exr::meta::attribute::AttributeValue; -use exr::prelude::{ReadChannels, ReadLayers, read}; use crate::Format; use crate::error::CodecError; @@ -200,7 +199,8 @@ pub(crate) fn decode(bytes: &[u8], layer_name: &str, limits: &Limits) -> Result< .iter() .find(|l| l.name.eq_ignore_ascii_case(layer_name)) .ok_or_else(|| CodecError::unsupported(F, format!("no Cryptomatte layer named {layer_name}")))?; - let meta = MetaData::read_from_buffered(Cursor::new(bytes), false).map_err(|e| CodecError::malformed(F, e))?; + // Also caps all parts' declared sizes together, like every other flat decode. + let meta = super::exr::read_meta(bytes, limits)?; if meta.requirements.has_deep_data { return Err(CodecError::unsupported(F, "this file has deep parts; Cryptomatte decoding stays with flat files")); } @@ -214,13 +214,7 @@ pub(crate) fn decode(bytes: &[u8], layer_name: &str, limits: &Limits) -> Result< let per_pixel = (layer.channels.len().max(1) as u64).saturating_mul(8).saturating_add(std::mem::size_of::>() as u64); limits.check_bytes(w32, h32, per_pixel)?; - let image = read().no_deep_data().largest_resolution_level().all_channels().all_layers().all_attributes().from_buffered(Cursor::new(bytes)).map_err( - |e| match e { - exr::error::Error::NotSupported(m) => CodecError::unsupported(F, format!("not supported: {m}")), - e => CodecError::malformed(F, e), - }, - )?; - let ex_layer = image.layer_data.get(layer.part).ok_or_else(|| CodecError::malformed(F, "the Cryptomatte part is missing"))?; + let ex_layer = super::exr::read_layer(bytes, layer.part)?; let mut ids: Vec> = vec![Vec::new(); layer.channels.len()]; let mut coverages: Vec> = vec![Vec::new(); layer.channels.len()]; diff --git a/crates/codecs/tests/exr_multipart.rs b/crates/codecs/tests/exr_multipart.rs index c8f9a3bc3f..f106d37791 100644 --- a/crates/codecs/tests/exr_multipart.rs +++ b/crates/codecs/tests/exr_multipart.rs @@ -197,3 +197,32 @@ fn parts_are_capped_together() { let e = exr_info(&aov_file(), &limits).unwrap_err().to_string(); assert!(e.contains("across 3 parts"), "{e}"); } + +/// Two ZIP1 parts of 4x4 RGBA; every chunk of part 0 is then overwritten with garbage, so +/// inflating it fails. +fn file_with_corrupt_first_part() -> Vec { + let px = |k: f32| -> Vec<(&'static str, Vec)> { ["R", "G", "B", "A"].iter().map(|c| (*c, vec![k; 16])).collect() }; + let mut bytes = write_image(vec![layer_f32("first", Compression::ZIP1, &px(0.25)), layer_f32("second", Compression::ZIP1, &px(0.75))]); + let mut cursor = std::io::Cursor::new(&bytes[..]); + exr::meta::MetaData::read_from_buffered(&mut cursor, false).unwrap(); + let table = cursor.position() as usize; + // Part 0 owns the first four of the eight chunks (one scan line per ZIP1 block). + for i in 0..4 { + let at = table + i * 8; + let offset = u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap()) as usize; + let size = u32::from_le_bytes(bytes[offset + 8..offset + 12].try_into().unwrap()) as usize; + bytes[offset + 12..offset + 12 + size].fill(0xA5); + } + bytes +} + +#[test] +fn decoding_one_part_does_not_decode_the_others() { + let bytes = file_with_corrupt_first_part(); + // Part 0 is unreadable ... + assert!(decode_exr_part(&bytes, 0, &DecodeOptions::default()).is_err()); + // ... but asking for part 1 must not touch it. + let img = decode_exr_part(&bytes, 1, &DecodeOptions::default()).expect("part 1 decodes on its own"); + assert_eq!((img.width(), img.height()), (4, 4)); + assert!(img.to_normalized().iter().all(|v| near(*v, 0.75)), "wrong pixels"); +} From a8b52f1dbfd4b7f0ace9c1ce0aaab22d5c6caa41 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Mon, 12 Oct 2026 16:34:45 +0200 Subject: [PATCH 17/27] Codecs: enable JPEG and CCITT fax compression for TIFF (#3712) (#3727) Co-authored-by: Claude Opus 5.5 --- crates/codecs/Cargo.toml | 2 +- crates/codecs/tests/tiff_regressions.rs | 55 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/codecs/Cargo.toml b/crates/codecs/Cargo.toml index c462d0a39b..25f3f09be0 100644 --- a/crates/codecs/Cargo.toml +++ b/crates/codecs/Cargo.toml @@ -22,7 +22,7 @@ heif = ["dep:photocraft-heif"] thiserror.workspace = true image = { workspace = true, features = ["gif", "bmp", "tga", "ico", "qoi", "hdr"] } png = "0.18" -tiff = { version = "0.11", default-features = false, features = ["deflate", "lzw"] } +tiff = { version = "0.11", default-features = false, features = ["deflate", "fax", "jpeg", "lzw"] } exr = { version = "1.73", default-features = false } half = "2" serde_json.workspace = true diff --git a/crates/codecs/tests/tiff_regressions.rs b/crates/codecs/tests/tiff_regressions.rs index a4524e0874..a7c1aa486e 100644 --- a/crates/codecs/tests/tiff_regressions.rs +++ b/crates/codecs/tests/tiff_regressions.rs @@ -1,3 +1,6 @@ +mod common; + +use common::tiffgen::*; use photocraft_codecs::{Image, decode, exif_orientation, upright_exif}; fn classic_white_is_zero() -> Vec { @@ -95,3 +98,55 @@ fn big_tiff_orientation_6_and_8_are_applied_to_decoded_pixels() { assert_eq!(image.data(), &upright, "orientation {orientation}"); } } + +fn with_compression(mut dir: Dir, code: u64) -> Dir { + for entry in &mut dir.entries { + if entry.0 == 259 { + entry.2 = vec![V::N(code)]; + } + } + dir +} + +#[test] +fn jpeg_compressed_tiff_decodes() { + // A flat mid-grey 16x16 image as a baseline JPEG stored in one strip (Compression = 7). + let mut jpeg = Vec::new(); + let gray = vec![100u8; 16 * 16]; + jpeg_encoder::Encoder::new(&mut jpeg, 95).encode(&gray, 16, 16, jpeg_encoder::ColorType::Luma).expect("encode the JPEG strip"); + let mut dir = with_compression(strips(16, 16, 8, 1, 1, 16, &[0; 256]), 7); + dir.chunks = vec![jpeg]; + let bytes = build(true, false, &[dir], &[0]).bytes; + + let image = decode(&bytes).expect("a JPEG-compressed TIFF should decode"); + assert_eq!(image.dimensions(), (16, 16)); + assert!(image.data().iter().all(|&v| v.abs_diff(100) <= 4), "{:?}", image.data().get(..4)); +} + +#[test] +fn ccitt_group4_tiff_decodes() { + // 8x3 bilevel: a white row, then two black rows. Bits: V0 | H(white 0, black 8) | V0 V0 | EOFB. + let bits = "1".to_string() + "001" + "00110101" + "000101" + "11" + "000000000001" + "000000000001"; + let mut data = Vec::new(); + for chunk in bits.as_bytes().chunks(8) { + let mut byte = 0u8; + for (i, c) in chunk.iter().enumerate() { + if *c == b'1' { + byte |= 0x80 >> i; + } + } + data.push(byte); + } + let mut dir = with_compression(strips(8, 3, 1, 1, 0, 3, &[0; 3]), 4); + dir.chunks = vec![data]; + let bytes = build(true, false, &[dir], &[0]).bytes; + + let image = decode(&bytes).expect("a CCITT G4 TIFF should decode"); + assert_eq!(image.dimensions(), (8, 3)); + let d = image.data(); + assert_eq!(d.len(), 24, "{d:?}"); + assert!(d[..8].iter().all(|&v| v == d[0]), "{d:?}"); + assert!(d[8..].iter().all(|&v| v == d[8]), "{d:?}"); + assert_ne!(d[0], d[8], "{d:?}"); + assert_eq!(d[0], 255, "white row of a WhiteIsZero image"); +} From dc35ce763009969434d873c2063f59d96660b52d Mon Sep 17 00:00:00 2001 From: Korg1ch <57859048+Korg1ch@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:35:47 +0300 Subject: [PATCH 18/27] fix(linux): avoid duplicate AppImage desktop launcher (#3623) (#3641) --- packaging/linux/AppRun | 50 +++++++++++++------ .../linux/ai.storyteller.photocraft.desktop | 3 ++ packaging/linux/apprun-test.sh | 26 ++++++++-- 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/packaging/linux/AppRun b/packaging/linux/AppRun index e8543ee2bb..42008b177b 100755 --- a/packaging/linux/AppRun +++ b/packaging/linux/AppRun @@ -35,25 +35,47 @@ integrate() { src_desktop="$APPDIR/usr/share/applications/$DESK_ID.desktop" [ -f "$src_desktop" ] || return 0 - # Rewrite Exec to this AppImage (the binary is not on the host PATH) and drop - # TryExec, which would hide the entry; everything else is shipped as packaged. + # If an external AppImage integrator (AppImageLauncher, appimaged, Gear Lever) is + # already managing this AppImage, do not write a second launcher (#3623); remove our + # own if left over from an earlier run, and still install the hicolor icons below. apps="$data/applications" mkdir -p "$apps" 2>/dev/null || return 0 - tmp="$apps/.$DESK_ID.tmp.$$" - esc="$(printf '%s' "$appimage" | sed 's/[\\&|]/\\&/g')" - if sed -e "s|^Exec=.*|Exec=\"$esc\" %F|" -e '/^TryExec=/d' "$src_desktop" 2>/dev/null >"$tmp"; then - if cmp -s "$tmp" "$apps/$DESK_ID.desktop"; then - rm -f "$tmp" - else - if mv -f "$tmp" "$apps/$DESK_ID.desktop" 2>/dev/null; then - command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1 - else - rm -f "$tmp" + external=0 + if [ -n "${DESKTOPINTEGRATION:-}" ]; then + external=1 + else + for f in "$apps"/*.desktop; do + [ -f "$f" ] && [ "$f" != "$apps/$DESK_ID.desktop" ] || continue + if grep '^Exec=' "$f" 2>/dev/null | grep -Fq "$appimage"; then + external=1 + break fi + done + fi + if [ "$external" = 1 ]; then + if [ -f "$apps/$DESK_ID.desktop" ]; then + rm -f "$apps/$DESK_ID.desktop" 2>/dev/null || true + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1 fi else - rm -f "$tmp" - return 0 + # Rewrite Exec to this AppImage (the binary is not on the host PATH) and drop + # TryExec, which would hide the entry; everything else is shipped as packaged. + tmp="$apps/.$DESK_ID.tmp.$$" + esc="$(printf '%s' "$appimage" | sed 's/[\\&|]/\\&/g')" + if sed -e "s|^Exec=.*|Exec=\"$esc\" %F|" -e '/^TryExec=/d' "$src_desktop" 2>/dev/null >"$tmp"; then + if cmp -s "$tmp" "$apps/$DESK_ID.desktop"; then + rm -f "$tmp" + else + if mv -f "$tmp" "$apps/$DESK_ID.desktop" 2>/dev/null; then + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$apps" >/dev/null 2>&1 + else + rm -f "$tmp" + fi + fi + else + rm -f "$tmp" + return 0 + fi fi # Icons: per-file compare so an unchanged install costs a handful of cmp calls. diff --git a/packaging/linux/ai.storyteller.photocraft.desktop b/packaging/linux/ai.storyteller.photocraft.desktop index 70ce82148e..a32b55cef5 100644 --- a/packaging/linux/ai.storyteller.photocraft.desktop +++ b/packaging/linux/ai.storyteller.photocraft.desktop @@ -13,3 +13,6 @@ StartupWMClass=ai.storyteller.photocraft Categories=Graphics;2DGraphics;RasterGraphics;Photography; Keywords=photo;image;editor;psd;layers;retouch;paint; MimeType=application/x-photocraft;image/x-paintnet;image/openraster;image/vnd.adobe.photoshop;image/x-psd;image/x-psb;image/png;image/jpeg;image/tiff;image/webp;image/gif;image/bmp;image/x-tga;image/x-icon;image/vnd.microsoft.icon;image/x-portable-anymap;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-exr;image/vnd.radiance;image/avif;image/qoi; +# AppRun registers the desktop entry and icons itself (#593); tell external +# AppImage integrators (AppImageLauncher, appimaged) not to add a duplicate (#3623). +X-AppImage-Integrate=false diff --git a/packaging/linux/apprun-test.sh b/packaging/linux/apprun-test.sh index 0c81a04052..475fb5ee5e 100755 --- a/packaging/linux/apprun-test.sh +++ b/packaging/linux/apprun-test.sh @@ -48,6 +48,7 @@ expected_exec="Exec=\"$(readlink -f "$APPIMAGE")\" %F" grep -Fxq "$expected_exec" "$DESKTOP" || fail "Exec not rewritten: $(grep '^Exec=' "$DESKTOP")" grep -q '^TryExec=' "$DESKTOP" && fail "TryExec kept, the entry would be hidden" grep -Fxq "Icon=$APP_ID" "$DESKTOP" || fail "Icon line lost" +grep -Fxq "X-AppImage-Integrate=false" "$DESKTOP" || fail "X-AppImage-Integrate=false missing (#3623)" grep -Fxq 'Categories=Graphics;2DGraphics;RasterGraphics;Photography;' "$DESKTOP" || fail "metadata lines lost" if command -v desktop-file-validate >/dev/null; then desktop-file-validate "$DESKTOP" || fail "generated desktop entry is invalid" @@ -71,23 +72,40 @@ export APPIMAGE="$TMP/moved.AppImage" "$APPDIR/AppRun" >/dev/null || fail "launch after move exited non-zero" grep -Fxq "Exec=\"$(readlink -f "$APPIMAGE")\" %F" "$DESKTOP" || fail "Exec not refreshed after move" -# 6. Opt-out installs nothing. +# 6. External AppImage integrator (#3623): if another .desktop file in $XDG_DATA_HOME/applications +# already launches this AppImage (e.g. AppImageLauncher's appimagekit-*.desktop), AppRun removes +# its own duplicate .desktop entry while keeping the hicolor icons installed. +ext_desktop="$XDG_DATA_HOME/applications/appimagekit-photocraft.desktop" +printf '[Desktop Entry]\nType=Application\nName=PhotoCraft\nExec="%s" %%F\nIcon=%s\n' "$(readlink -f "$APPIMAGE")" "$APP_ID" >"$ext_desktop" +"$APPDIR/AppRun" >/dev/null || fail "launch with external integrator desktop entry exited non-zero" +[ ! -e "$DESKTOP" ] || fail "duplicate desktop entry kept alongside external integrator entry" +[ -f "$ext_desktop" ] || fail "external integrator desktop entry removed" +[ -f "$XDG_DATA_HOME/icons/hicolor/256x256/apps/$APP_ID.png" ] || fail "icons missing with external integrator" +rm -f "$ext_desktop" + +# 7. DESKTOPINTEGRATION set by an external integrator skips creating a second .desktop entry. +rm -rf "$XDG_DATA_HOME" +DESKTOPINTEGRATION=AppImageLauncher "$APPDIR/AppRun" >/dev/null || fail "DESKTOPINTEGRATION launch exited non-zero" +[ ! -e "$DESKTOP" ] || fail "DESKTOPINTEGRATION still wrote duplicate desktop entry" +[ -f "$XDG_DATA_HOME/icons/hicolor/256x256/apps/$APP_ID.png" ] || fail "icons not installed under DESKTOPINTEGRATION" + +# 8. Opt-out installs nothing. rm -rf "$XDG_DATA_HOME" PHOTOCRAFT_NO_DESKTOP_INTEGRATION=1 "$APPDIR/AppRun" >/dev/null || fail "opt-out launch exited non-zero" [ ! -e "$XDG_DATA_HOME" ] || fail "opt-out still integrated" -# 7. An unwritable data dir never blocks the launch. +# 9. An unwritable data dir never blocks the launch. mkdir -p "$TMP/ro" && chmod a-w "$TMP/ro" out="$(XDG_DATA_HOME="$TMP/ro/data" "$APPDIR/AppRun" out.txt)" || fail "launch blocked by unwritable data dir" [ "$out" = "stub: out.txt" ] || fail "stub not run against unwritable data dir: $out" chmod u+w "$TMP/ro" -# 8. No $APPIMAGE (extracted AppDir): launch only, nothing installed. +# 10. No $APPIMAGE (extracted AppDir): launch only, nothing installed. rm -rf "$XDG_DATA_HOME" env -u APPIMAGE "$APPDIR/AppRun" >/dev/null || fail "extracted-AppDir launch exited non-zero" [ ! -e "$XDG_DATA_HOME" ] || fail "integrated without $APPIMAGE" -# 9. A path that would need Exec escaping (here a `$`): launch only, nothing installed. +# 11. A path that would need Exec escaping (here a `$`): launch only, nothing installed. rm -rf "$XDG_DATA_HOME" odd="$TMP/odd\$(id).AppImage" : >"$odd" From f4fa53140cb005854021a8ef69b8b6a4c09e3ffc Mon Sep 17 00:00:00 2001 From: Streetblock Date: Mon, 12 Oct 2026 16:43:07 +0200 Subject: [PATCH 19/27] ops: count selection masks in history pixel budget (#3614) * ops: count selection masks in history pixel budget * test: expect unclipped HDR levels in float documents * web: optimize engine code for the wasm size budget --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- crates/ops/src/lib.rs | 88 +++++++++++++++++++++++++++++++++++++++-- crates/ops/src/tiles.rs | 3 ++ 2 files changed, 88 insertions(+), 3 deletions(-) diff --git a/crates/ops/src/lib.rs b/crates/ops/src/lib.rs index 70cbebe0f7..2a82edb417 100644 --- a/crates/ops/src/lib.rs +++ b/crates/ops/src/lib.rs @@ -5,8 +5,8 @@ //! stores whole-document snapshots per transaction, which is simple, obviously correct, and the //! same approach Photoshop's History panel exposes to users (one state per step). //! -//! Memory is bounded by `max_states` plus an approximate byte budget. Only tiles *not shared* -//! with the current document count against it. +//! Memory is bounded by `max_states` plus an approximate byte budget. The current document's +//! tiles and tiles only history holds count against it, including selection masks. #![forbid(unsafe_code)] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] @@ -248,7 +248,7 @@ impl History { } } -/// Bytes of the pixel tiles of `doc` (layers, masks, alpha channels) not already in `seen`. +/// Bytes of the pixel tiles of `doc` (layers, masks, alpha channels, selection) not already in `seen`. fn tile_bytes(doc: &Document, seen: &mut HashSet) -> usize { let mut n = 0usize; tiles::surfaces(doc, |surface| { @@ -408,6 +408,85 @@ mod tests { assert_eq!(h.unique_bytes(&cur), 256 * 256 * 4); } + #[test] + fn selection_tiles_count_current_and_history_without_double_counting() { + use photocraft_doc::{PixelFormat, Surface}; + for depth in SampleType::ALL { + let mut doc = Document::new("selection", Size::new(300, 64), ColorMode::Rgb, depth); + let mut selection = Surface::new(PixelFormat::new(ColorMode::Grayscale, depth, false)); + selection.write_pixel(1, 1, &[1.0]); + selection.write_pixel(257, 1, &[1.0]); + doc.selection = Some(selection); + let mut cur = Arc::new(doc); + let tile = match depth { + SampleType::U8 => 65_536, + SampleType::U16 => 131_072, + SampleType::F32 => 262_144, + }; + let mut h = History { max_bytes: usize::MAX, ..History::default() }; + assert_eq!(h.pixel_bytes(&cur), 2 * tile, "current selection counts without any history"); + assert_eq!(h.unique_bytes(&cur), 0); + edit(&mut h, &mut cur, "Rename", |doc| doc.name = "renamed".into()); + assert_eq!(h.pixel_bytes(&cur), 2 * tile, "the shared selection tiles count once"); + edit(&mut h, &mut cur, "Selection", |doc| { + doc.selection.as_mut().unwrap().write_pixel(1, 1, &[0.5]); + }); + assert_eq!(h.pixel_bytes(&cur), 3 * tile, "only the changed selection tile is copied"); + assert_eq!(h.unique_bytes(&cur), tile); + h.trim(&cur); + assert_counter(&h, &cur); + let selected_value = cur.selection.as_ref().unwrap().sample_channel(1, 1, 0); + edit(&mut h, &mut cur, "Deselect", |doc| doc.selection = None); + assert_eq!(h.pixel_bytes(&cur), 3 * tile, "deselecting leaves the previous masks in history"); + assert_eq!(h.unique_bytes(&cur), 3 * tile); + assert_counter(&h, &cur); + cur = h.undo(cur).unwrap().0; + assert_eq!(cur.selection.as_ref().unwrap().sample_channel(1, 1, 0), selected_value); + assert_eq!(h.pixel_bytes(&cur), 3 * tile); + assert_eq!(h.unique_bytes(&cur), tile); + assert_counter(&h, &cur); + // Coalescing can change the current mask without recording another state. + Arc::make_mut(&mut cur).selection.as_mut().unwrap().write_pixel(257, 1, &[0.25]); + assert_eq!(h.pixel_bytes(&cur), 4 * tile); + assert_eq!(h.trim(&cur), 0); + assert_counter(&h, &cur); + cur = h.redo(cur).unwrap().0; + assert!(cur.selection.is_none()); + assert_eq!(h.pixel_bytes(&cur), 4 * tile); + assert_counter(&h, &cur); + } + } + + #[test] + fn selection_byte_budget_drops_old_masks_and_keeps_the_last_undo() { + use photocraft_doc::{PixelFormat, Surface}; + for depth in SampleType::ALL { + let mut doc = Document::new("selection", Size::new(64, 64), ColorMode::Rgb, depth); + doc.selection = Some(Surface::new(PixelFormat::new(ColorMode::Grayscale, depth, false))); + let mut cur = Arc::new(doc); + let tile = 256 * 256 * depth.bytes(); + let mut h = History::default(); + for i in 1..=6 { + edit(&mut h, &mut cur, "Selection", |doc| { + doc.selection.as_mut().unwrap().write_pixel(1, 1, &[i as f32 / 8.0]); + }); + } + assert_eq!(h.pixel_bytes(&cur), 6 * tile); + h.max_bytes = 3 * tile; + assert_eq!(h.trim(&cur), 4); + assert_eq!(h.past_len(), 2); + assert_eq!(h.pixel_bytes(&cur), 3 * tile); + assert_eq!(h.unique_bytes(&cur), 2 * tile); + assert_counter(&h, &cur); + h.max_bytes = 1; + assert_eq!(h.trim(&cur), 1); + assert_eq!(h.past_len(), 1, "the most recent undo survives even below one mask's size"); + assert_eq!(h.pixel_bytes(&cur), 2 * tile); + cur = h.undo(cur).unwrap().0; + assert_counter(&h, &cur); + } + } + fn assert_counter(h: &History, current: &Document) { let counter = h.tiles.as_ref().expect("budget initializes the counter"); let total = tile_bytes(current, &mut HashSet::new()).saturating_add(h.unique_bytes(current)); @@ -506,11 +585,13 @@ mod tests { fn counter_tracks_eviction_undo_redo_branching_purge_and_clones() { for depth in [SampleType::U8, SampleType::U16, SampleType::F32] { let mut cur = Arc::new(Document::with_background("h", Size::new(300, 64), ColorMode::Rgb, depth, Color::WHITE)); + Arc::make_mut(&mut cur).selection = Some(photocraft_doc::Surface::new(photocraft_doc::PixelFormat::GRAY8)); let mut h = History::new(4); h.max_bytes = usize::MAX; for i in 0..8 { edit(&mut h, &mut cur, "Paint", |doc| { doc.layers[0].surface_mut().unwrap().write_pixel(1, 1, &[i as f32 / 10.0, 0.0, 0.0, 1.0]); + doc.selection.as_mut().unwrap().write_pixel(1, 1, &[i as f32 / 10.0]); }); h.trim(&cur); if i >= 1 { @@ -547,6 +628,7 @@ mod tests { gray.write_pixel(1, 1, &[1.0]); doc.layers[0].mask = Some(LayerMask { surface: gray.clone(), ..LayerMask::reveal_all() }); doc.channels.push(AlphaChannel::new("alpha", gray.clone())); + doc.selection = Some(gray.clone()); doc.quick_mask = Some(AlphaChannel::new("quick", gray)); doc.layers.push(doc.layers[0].clone()); let mut cur = Arc::new(doc); diff --git a/crates/ops/src/tiles.rs b/crates/ops/src/tiles.rs index c4178b5b31..feb639c871 100644 --- a/crates/ops/src/tiles.rs +++ b/crates/ops/src/tiles.rs @@ -30,6 +30,9 @@ pub(crate) fn surfaces(doc: &Document, mut visit: impl FnMut(&Surface)) { for channel in doc.channels.iter().chain(&doc.quick_mask) { visit(&channel.surface); } + if let Some(selection) = &doc.selection { + visit(selection); + } } impl Tiles { From 9163afbdcfa8f6ab5e7b6967c2aa98eab9d8c608 Mon Sep 17 00:00:00 2001 From: zenjabba Date: Mon, 12 Oct 2026 10:45:05 -0400 Subject: [PATCH 20/27] Verify PngSuite archives and cached corpus files (#3973) Co-authored-by: zenjabba <679864+zenjabba@users.noreply.github.com> --- xtask/pngsuite.sha256 | 178 +++++++++++++ xtask/src/corpus.rs | 538 +++++++++++++++++++++++++++++++++++++-- xtask/src/corpus_pins.rs | 4 + 3 files changed, 696 insertions(+), 24 deletions(-) create mode 100644 xtask/pngsuite.sha256 diff --git a/xtask/pngsuite.sha256 b/xtask/pngsuite.sha256 new file mode 100644 index 0000000000..a13873a568 --- /dev/null +++ b/xtask/pngsuite.sha256 @@ -0,0 +1,178 @@ +71e1f5bf865131b5d9996e76a78372146283e5089e50f900ae927a6458b62101 PngSuite.LICENSE +a2853f2be68e460dcb9992819125b74503a01383e79f79a04b971ffa9d248544 PngSuite.README +6cf3bcd1757bfad2a7ce9c9659d4f609297a0828cafc7c9eddee18c5576ba9e9 PngSuite.png +9bf1c4610280efecc291361e2b7b026d602d03a7137f476ff84af626bb7b3a62 basi0g01.png +50ed4ff9c1c80578ae58ead6fb28ea2e44016b5b56a7b3e95c235ec86f97c0d8 basi0g02.png +0cce4212157dae7c0f71f5f72cef39cecab3347de455da0ace7de83eebd47849 basi0g04.png +b9300750e7414c9fed68cc7f4f531f5e2cdd2724f50142e8aa3a0070836bece9 basi0g08.png +d354026bdd0aa6ed7bedea7c92e5eef2ebbd4854848b8a0a48f562cc71bfe80f basi0g16.png +b2690d4475cdc39faf5a7d2de20de4e14eb96c6360fa791ec2003b4085ecacde basi2c08.png +ae13eba5fabbf7cbe8c01ec4458cdaa7e6bfdfcbd5109e831efaac9f39096027 basi2c16.png +8e1edfbc0382e0b545c70209271cedead2d530df1ce40f5296f0749226b36006 basi3p01.png +85f304eaaf6831b2bcc2c08892b947fef52f06337aaf23312d6326e367d73f77 basi3p02.png +78890ee2c4da71e460b5a1527efe7dc17d673ebe997f49ca1b4dcd7347d21e56 basi3p04.png +ca103eef7a7c50abc7af9d5ec0dc9fdc07668e2f030adbce226e050dcec9eea3 basi3p08.png +dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b basi4a08.png +ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e basi4a16.png +fe70ea20c8447e82abb7bc9dd05344a61a4631cc69e0974378df8f6263af0337 basi6a08.png +7fdd6bf08f04692bcf06b5c4262e7f76c49d8adb992789fd3f05aa0435645cd8 basi6a16.png +c8b1364d7771dd2f5a1b2d7d633abcf3f48dafee608558ecd2e5fc98f61894cd basn0g01.png +ca172e92d179c77b3d321ad501e452a59bdaafafde8ebd74fd0669c4eae58efc basn0g02.png +fb2d42c4c8f5c51590e675994a9b0c718fbf71c80e28142f231cfa7c5b0e26e8 basn0g04.png +268d061075d1dd2eeec62b31303d09f6998549e1bfb447a5f09c80a2b0978ac3 basn0g08.png +1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 basn0g16.png +c90e86090a625661b19960cafdde6e347d6e32d73837aaae533f66dd3f099506 basn2c08.png +1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b basn2c16.png +c2d7cd682df5f74506b33a5d70c344aaee248fda79fdfef8e873426fd6f2b75b basn3p01.png +0466bb7ed9984cf03b70704564bcffab1df8ec0e8167473ba0f75e4fedce5a8f basn3p02.png +e1fc7be978d3149b98533d0076245ae64353b7967290f4204c1282ecb4ec1aba basn3p04.png +d58256cd2eb16b5740d4c1403d25ce43d8dd03e270627ab709d2fb141e3d904c basn3p08.png +7be24b618bfd437b921ae0caf9341d112338c6290cc62466deb19d2c7a512968 basn4a08.png +1c92ffd11d2fc89a36f170d0239668436407f5c8e3f8d93483fb3fc6bca361d7 basn4a16.png +559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 basn6a08.png +8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa basn6a16.png +dfae01a4548e30519157bb51ce29d4de3b35b74288e9568b4d5911de01c6048b bgai4a08.png +ef7df23ccd912309a4f89ca3c3094bf5ab55add25218e571e51cc6cb11cfad9e bgai4a16.png +559c594166eb156f461c9beff0f053196730dc998fdb0d2b801c89e6680860a5 bgan6a08.png +8f9d81060aebf4576461403c5057de7f23f73157016b659402b906df805845aa bgan6a16.png +71d041df5c8949ce07d5e93b3c9f752cefc75ef7326c49df05ae73915f1a755f bgbn4a08.png +c45de44d3442bd33072d68456541b10e0cc57c5d8bfc809208995d939091eb2d bggn4a16.png +2da3c3efa01bf166958852a104ece92daaede8ffb5e0452a55d40526258a8bf5 bgwn6a08.png +875bc2b44a4355551c51260def9664da68be1af8c14b82f76173aaf01ddb1929 bgyn6a16.png +c88909e74e039dd3df829bf24144b487171e53b17f5c5c07cbc08688247d24b5 ccwn2c08.png +dc365b39d49c287669d837872dd59aef30763a611dfc8c046c481e35e49a390a ccwn3p08.png +438018f19c85e582cb586ac7cca2220008ecb7fc70ce50e8f2a76b494c128a20 cdfn2c08.png +a068eaf1f7040490e08eda3259befb6689849dd0ff8bb4cc03c705d117cb2b9f cdhn2c08.png +ee08dfa303a3c09171280fc1821180dc0a4c3591af7e9077e8ce0435642c76d2 cdsn2c08.png +a849d89ba3d92146940d7c56d2fe74c9c3ec96725c7f24eefaf44160a6304de2 cdun2c08.png +587a8624228d3f605f475bd751a1f3a97f0bbab590c0ff518bce5da3abcb6f2b ch1n3p04.png +e3cace549047c6e5e693b9b135d53f7f9ca5b1088bf30cc408634598ba53758a ch2n3p08.png +79ccaf4abe5091e1f65f8a61496363ed3312ebb9ea1aaa98affadac3d98507de cm0n0g04.png +127bb50d224ac3ebff36160482a03b9e6686072e86c8ff4cd43e3ce2271b2315 cm7n0g04.png +774b384d0ad73698e7d93fc5a29976b332c619045e5966276c609b11459a4152 cm9n0g04.png +bc812de2b83abc9ae2a6d1ae01e91f9090214b9ed8fee1d56cdfea58f4f259d0 cs3n2c16.png +267104dced0858dd828cdd41edc0e87042c7094c77191565192a67045064b23f cs3n3p08.png +028fa49fbba3954c2eba27a4c3607196e87c1dae74192df3b3ad7afe5e0a3f0c cs5n2c08.png +a7a4021983d6b22685529cbafb65cd087c9e1af1c5d205c2e13e3efc4ae87b6a cs5n3p08.png +d90ddab7313e4e1fd7f20f16a6c546b449697fbcac1a7776298a74896f4662da cs8n2c08.png +5bffdb88e307a851f8e85b137a69c59e557a881db816e990061934c0155c894c cs8n3p08.png +081d1ec26b4157fbc032b76dc716321420f2d032a425de046557c7842766826d ct0n0g04.png +259116f8ecf849d83d824688eb02b4575aed11980f02ab6fb123281c25a6459d ct1n0g04.png +4583e63d1bdfa18b6abb47439dee9a73bed311ed1b0a71d22bed75fe4767832b cten0g04.png +d0607280c3539a8934c5cfd22788b382cdc31321cdedc75ae8a29e3948580ba6 ctfn0g04.png +5f4c87f5a8f589d029a908050880ac5d6aed3810e55e75444802d6d3fb27cf33 ctgn0g04.png +8ef799e59871755578e563c5b411bc3895c2f7112c115aaa921b6c04fe255bf2 cthn0g04.png +ca91332ecc04e5faac1f39ca4df4996b072a93f3e574e7f4b50d52aafbfe67b6 ctjn0g04.png +c0765e635a6423ec64f0314400c3df20745c4a4469ffcc323c7b00ba66b4aceb ctzn0g04.png +d04140d74bc60597c47b5aac371a3553d3add9354437bbd8c57007d94e197809 exif2c08.png +d055cc0bb505b37c6ecf88a808d71675eca5dd0ee683d70ebc9c0be0e1bb8e57 f00n0g08.png +0fe92b2aa2da04c885d1dbd85c834716f6cdd946364d97dcd597bb79d9e14427 f00n2c08.png +956ddaf133e6d9a8d36f1e00604d87fbc2d0295e933bb8caaef517e7d87e5342 f01n0g08.png +ef072ec6815ebf9b33e0553d2e4e4e7ed6911860a2512c67bcd10a9f0f09b9de f01n2c08.png +1f5b49c06ecc2a1756f0423b3de936bd18497866d035ac8eded78a6f17408a2c f02n0g08.png +ca4b937b3c587d5c007f193a2eec14dc96b0d23ff7d6aa9004e3badd1af9fe8f f02n2c08.png +d0563ae30c6ce7a01eae7a5cdc0ef780175bfa7653888ed93f17bef5fe4b2107 f03n0g08.png +2d101e3ef4f78a69437034671e93fe11faac0cfc4d44210dcca1b944caa886f7 f03n2c08.png +fec1a3c53833d0ddefa4486a98934c393cb4335b2dc31da70e6540a4686f1457 f04n0g08.png +c365c24153cb69fd3c162f00b296ae23a71a1595645d1aeb0ad23af680d7b4be f04n2c08.png +8ac0f095d2a943157e820fa121bccde08d5230af1b5830c3041d5f4da3524eba f99n0g04.png +3494b914dd1b094afa9a74a89bfa75219030e21af6fca3ed66c9d45032a047e9 g03n0g16.png +abce774b9624952c2d0c57ff681790ba620cf5616fe7e0ef0600600cdce3c22a g03n2c08.png +541389db0c72721c6c49501e1dbf6e32df41b4fcfa318676955e278f3f4332b3 g03n3p04.png +72350a2d9db2df09b626f1f1dd48061e9c749d471bf19814d2c5c10acbf007e5 g04n0g16.png +bfd3edfc4d85a43383556a51023533fa3888184aa53a6b43769910782beebfce g04n2c08.png +5ea85ebefb58b2ab113fc05bf1cb79ffb068359e3a5620843590a0a7b59bd49a g04n3p04.png +be19721d28e0b268af1e29e8190c952b94315bd08899383ae2c91e6dc8c4f889 g05n0g16.png +74ab5b8477992d65e220eeaff8df466522c1b392fa90c6f9935da5bdb7113c9d g05n2c08.png +2f46afd7ec15836b523cbce6965f34d89833b8d777afffaf0f53bce620d5f65e g05n3p04.png +9070f843981f765cc8e26a63751be5d58631d85a8a7b4e826358f80e8a679d03 g07n0g16.png +ca4cd32d222f65fb7beb7c9f40d8b6833b36d552b830ff1a1d67dae73b8ed0d8 g07n2c08.png +b8e7abeabcfd50b71cca78b6cdff7372310cfc179cd63304c3e904f469b333cb g07n3p04.png +a22486acb74ee5f947db3a20b1f314a9dd8aca3339840049ee559f4d676a6b5c g10n0g16.png +4ac23729aea109e8f6b1e831448115cf02dcd97559e9403847cbe6dd7e5c7347 g10n2c08.png +36cc2ea4b5b33cd18e0a01c7de85dbcaf7161d258cd0f2265a90cd835b15cf6d g10n3p04.png +1fae707d809296d2cf0d6aa653bead6199644fb1c859558ec7d2d69e709a423a g25n0g16.png +9b128cfa1bb417dd99251914d729062621fcea9f346168549b16a75fe030b0f6 g25n2c08.png +4197a0a25c4f74d42c2285d75b9fabe1eedbed45bc224ff1ec1784f5797e9dbf g25n3p04.png +1a1fe155f40c11d79350b3f84c7eb2b29719037e57073cb7ae2ab9e8323f9360 oi1n0g16.png +1e1c28ec0ee5da6224404b4005076dd30e2335a1a80c0ca722046a245c4a2b3b oi1n2c16.png +142bc4b1e9f8a9e3b0ba3d2c2025e485bf7e76df88f914bf8c9041b4562cc080 oi2n0g16.png +2c523f138e0ade1bed8f31d59ec0f34b82660b192d6af8b8f395f82b9ba9dabb oi2n2c16.png +aa2d77723ba44f654a41a2ab97e01271fe942fecd2f299c0a81da90acb2887ac oi4n0g16.png +661220a0ed03ca6d63ec8dcaac738d4a9fd089b491c40dd896267ffb6cf5015d oi4n2c16.png +ae70c9effd0d9957a0d42771bc987529a1f3715e068057fae63f0c7e50d5c53a oi9n0g16.png +fef98e9bebe6c4046c29f2568f7a735305c8661aa01f9c37a718135e5bc4f941 oi9n2c16.png +72406b6173ec20dd059e8cea594ef5bc8f81bac5e416e2b53058d2debd51d195 pp0n2c16.png +d5a6f964a601a5e1815525117c12c32b61e2aa09fd6040776d35f2f7115b7110 pp0n6a08.png +b321b9ac8f2aa641475b6936365954181726dcb8662dbf641284d82a83cc22a5 ps1n0g08.png +6c59d673032fe2d04dbd2ea91cc9bc7bebe7d5ab84911f00e36fb28994793576 ps1n2c16.png +bd2ae1b45fd771aaa8c57dccd183f3834fc9742f6dfc692606865049efb116e5 ps2n0g08.png +120c62ce9d014f7772c4b38a446991b5bbe6a22ef5ea59239d32e023bc167585 ps2n2c16.png +235eb82f1337e3fcefbdca566a53af3d3523db7588ba3cd7fa5dab19afe48a41 s01i3p01.png +6a4f6da9fe36d195219ad2beeac1a0423e2ee6de03dfb064a3b6fa31623eb230 s01n3p01.png +e7d26ab4b31e7066b0674a4374c093967a9f1761b96836aa3dbe624acd73ab52 s02i3p01.png +f4b324f2b1d013ee77f632e42129c7cd3eca55da846e775e744a1f224ad1efcd s02n3p01.png +4513dec80f54ffd04a28834b114254d6e6d66e52e32e0059174474f2fb15d6bc s03i3p01.png +4dee297bd7f747f87488509af11a1e1e0531fa67e6e8d0f8ab7f4df7940f0ff3 s03n3p01.png +918f94f3557e10de91efa680df507c8eafc347fa5023af43037701398128ad0f s04i3p01.png +e73c52f764b684ec5133cce11f7f18cc60065acdd308e2c3d871e0a792a1080e s04n3p01.png +77fe040e17167f9656afe46cf0031832e635745e4e7f19ba12dee32f5ec03d96 s05i3p02.png +1b9e244a5c1d6cc9830391fff4eac565ada026d000e60faf7525c8add6bdc726 s05n3p02.png +cf8d5fdb34c0afd38b1b4745bed6167771767ccdfe847c4d8766f7fcc5c41176 s06i3p02.png +5caabfc4b147fad5e49e558143faaab91888324240acf45e5db8fc49f7a7f667 s06n3p02.png +d9874bfad10353a9ea2876ee388c3e7f81f697ed141d0d79c5c2019d62a7a698 s07i3p02.png +e7d447394f5fa64c27353216af387352a77a1fb534922a2d974e60d965d4e414 s07n3p02.png +62adf855c6b1e3a007f9dcf3ce2ba244616c089269a511dd939e89a4b0853453 s08i3p02.png +544fdd476e566e8aef4c98604124ae05acef48be20d1aebb9b6674b08b2abbb6 s08n3p02.png +f2cf5b3109690704d106a0b22ea079e36064cc6077e1fb9a1ba87397d0bbcca1 s09i3p02.png +6be8f58b638e7f7114750f5796f66f650db8fb7242a36183850f02117f4eaa65 s09n3p02.png +a27e0dbf923ef868e7399560b76c23a50b2d8926cddd9775e2bc4e050273f205 s32i3p04.png +c80d9fc7ded0b7834ec11d0e324540c7be74016b61bc302a1d167218fa35fbf9 s32n3p04.png +cc6ad98dcf686d92f1165d951f90fac70e568cedb1fcbba33e8890d0e6cdad4d s33i3p04.png +998d08e084e3eea288984475bfed0d48ee2d8250e20b96755898b59152e36a34 s33n3p04.png +83eb2e82eb14ae3c608833ef371a6f472c4c6e63b7e9f4f92f5472f975793b97 s34i3p04.png +a25451168c0b03ab8f52dcc60c6b9c12bc5df5d55afabe1892b87d0a856bb4a0 s34n3p04.png +db5988c27ad577de1e494cb50229dc2772678531adde2db84493907eae003e54 s35i3p04.png +cd99fc4cc69bda1ff8e8697d8b423ae8d9e84a58ba1d2b480751953b6869e3b3 s35n3p04.png +a36b40927b730a31d60c7818fb4a32e67ace06393cdaa26edc09520b6cee066d s36i3p04.png +ebc08f6a1e0918c5d3a9a08e184c23019a3cf90d8843e4b71bf2ae16040b1a84 s36n3p04.png +565c79404c4b9e46c27edb2f93d7b26b93ec0a0481ab7a21fc405152a21f53ba s37i3p04.png +964078b1f3f472268b8ac97a8976b4d1cdb6a47e6243d67eddcb0205e65b9f61 s37n3p04.png +b107f120ffaffcb949ac4547e0f91555ac681f78c7e68c724e7ceddfd8e5387b s38i3p04.png +045b0f5ea35874741bf6cac40f2e63ec8726450ab241475df4b7e9303c39e339 s38n3p04.png +4b5a6d8c04c8c70e1de3bcb6f7b5cc3ba359b13e28de6f52e21a291d4bc8a937 s39i3p04.png +2115b6cad0140ea97fe6ec056b47307ef1e8b5c00db5f08707a0572b165e5f08 s39n3p04.png +48a3f19b5c3b2f7eca17fd34765a2a3e385d770f24c357bc66bd74417eb60a5a s40i3p04.png +c4598ea73b1ec015bef7dcfd7f67bc7bfc41546619953d1a92cd9b8657a7033a s40n3p04.png +c5e7eeaffa677bfac002ee968a0423e0fea3be7701d39ccd6a015df1d2d85e40 tbbn0g04.png +8fcfd031a5b716201854fdab25691feed6cd3ed7b713a51b94e1f43fc63fe0c1 tbbn2c16.png +8eb43a6020fda8ed7c8c9b88b974ef794ee8452db29212ae7e5c11003bfd76a3 tbbn3p08.png +49711ee271f96099521b83b75d58ed2f76d42ed46cb3c06656fde2cad697cf59 tbgn2c16.png +6f798628935a021fe446dce3d62861d1f7369aaf01bad10daa4917c5abccad7d tbgn3p08.png +8223d8b0c5f84b149f0c1f0e1f658346e07fcbd03ffc02b2e9eeea1481eb19bf tbrn2c08.png +01e86f108b244bdee4d72e384aced8a62369965ca2993ee6ef88f131c647f7d1 tbwn0g16.png +978b28cec43bd219517083b63661bb30a7b9cdf9fd4476ab4e70844565980d51 tbwn3p08.png +7b4eb64b4bd7eb36417752873ddaca027ce77a07f474b2af569f4eea1e984111 tbyn3p08.png +f05e317f64a038123faf96fdbd6fc3ab3eec14e1fc1eb87fd7da4d996be63526 tm3n3p02.png +a253e055d8cd94049cd95e9392e7aa60f3d5c922a8f0cccb84762b0c5f547545 tp0n0g08.png +5dced3fde56b950e61ce86f4ad54458336f66389ce8ca68524d4fd8ec546ff5b tp0n2c08.png +da40ddb18bd45db32ed03dc89e2250b096d808efe975a88477e4f5a91a4677c9 tp0n3p08.png +50bc927fe6cf1816454a7c27f8c743734be8db45e37e5ee3ede540f4e895e6c5 tp1n3p08.png +4059f7e6a1c5bac1801f70e09f9ec1e1297dcdce34055c13ab2703d6d9613c7e xc1n0g08.png +e252a0e7df3e794e52ce4a831edafef76e7043d0d8d84019db0f7fd0b30e20f4 xc9n2c08.png +3c0c2a68dd416a7be79400e0e2ceb358ba3191d5309d4bad9dd04b32ff6d5b60 xcrn0g04.png +71e4b2826f61556eda39f3a93c8769b14d3ac90f135177b9373061199dbef39a xcsn0g01.png +c1287690808e809dc5d4fb89d8a7fd69ed93521f290abd42021ca00a061a1ba4 xd0n2c08.png +00b53c3bbd0641454521b982bc6f6bcfda7c91f1874cefb3a9bac37d80a1a269 xd3n2c08.png +16e5b40fb2600db1af20fb79ff715c2869255e2d4bef20702f16534e5dd6a847 xd9n2c08.png +f9d1fb2a708703518368c392c74765a6e3e5b49dbb9717df3974452291032df9 xdtn0g01.png +318864720c8fc0dbe4884035f2183cc3bd3d92ec60d447879982942600e9fe2e xhdn0g08.png +968fd21abb8acf40fbca90676de3abcb3a6e35b01ba7a3b9190184eafb99c83d xlfn0g04.png +776701227c7094dd10b78f508430b8ea4f03471d072096382cbcca628dea1d2b xs1n0g01.png +9dd7e93ba9211f0caee09d8f12e37742936c469f45fcad21e577c9f0640cf99e xs2n0g01.png +166c8633d116d1c26631cb98f3a75340cf4385fa7ae6a67913bc78e709bcb30d xs4n0g01.png +7a380568beeac969908196ac89cd2347c96ac2f26fdc2b8e0314b62c59f0e308 xs7n0g01.png +e4767d3259e1b331c9ece9fb07fc30f385879ac12b3cd0e1cb923f3bfac81dcf z00n2c08.png +939abfc6e0a3e349bb5bcd8c4e427a6cdb2c7e0c23a773aefa1c4fc9f07e4cb4 z03n2c08.png +21652fe42f472acd83dcb207806add5737db0020b21d6b38b0a00be3e9e37990 z06n2c08.png +98d90a7e61aa1a9ac9d99af44dd25c70cd5a7b4a11a85da60f82923e18a78f9f z09n2c08.png diff --git a/xtask/src/corpus.rs b/xtask/src/corpus.rs index 43f943c807..e6c929a1be 100644 --- a/xtask/src/corpus.rs +++ b/xtask/src/corpus.rs @@ -1,10 +1,15 @@ //! `cargo xtask corpus [...]` (fetch test corpora) and `cargo xtask test-corpus [...]` (fetch, //! then run the corpus tests). Pins: `corpus_pins.rs`. Docs: `docs/development.md` › Test corpora. -use std::path::PathBuf; -use std::process::Command; +use std::collections::BTreeMap; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; -use crate::corpus_pins::{self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_URL, PSD_TOOLS_COMMIT}; +use crate::corpus_pins::{ + self, AG_PSD_COMMIT, HEIC_RS_COMMIT, OPENEXR_COMMIT, PHOTOCRAFT_CORPUS_COMMIT, PILLOW_HEIF_COMMIT, PNGSUITE_SHA256, PNGSUITE_URL, PSD_TOOLS_COMMIT, +}; use crate::pinned::{PinnedCorpus, USER_AGENT}; use crate::{cargo, root, run}; @@ -119,7 +124,7 @@ fn status(present: bool) -> &'static str { fn list() { let corpus = root().join("corpus"); let png = corpus.join("pngsuite"); - let png_ok = pngsuite_count(&png) > 0; + let png_ok = verify_pngsuite(&png, PNGSUITE_MANIFEST).is_ok(); println!( "Test corpora live under {} (gitignored, never committed). The corpus tests are opt-in (cargo feature `corpus`) and fail, not skip, when a corpus is missing. CI always runs them. @@ -162,31 +167,212 @@ Pins: xtask/src/corpus_pins.rs. Moving one: change it, then -- --update-ma ); } -fn pngsuite_count(dir: &std::path::Path) -> usize { - std::fs::read_dir(dir).map(|d| d.flatten().filter(|e| e.path().extension().is_some_and(|x| x == "png")).count()).unwrap_or(0) +const PNGSUITE_MANIFEST: &str = include_str!("../pngsuite.sha256"); +const PNGSUITE_MAX_ARCHIVE_BYTES: u64 = 1024 * 1024; +const PNGSUITE_MAX_FILE_BYTES: u64 = 4 * 1024 * 1024; +static PNGSUITE_STAGE_ID: AtomicU64 = AtomicU64::new(0); + +/// Do not trust cached filenames, symlinks, or a file's size remaining unchanged during a read. +fn pngsuite_bytes(path: &Path, limit: u64) -> Result, String> { + let meta = std::fs::symlink_metadata(path).map_err(|e| format!("{}: {e}", path.display()))?; + if !meta.is_file() || meta.len() > limit { + return Err(format!("{}: expected a regular file of at most {limit} bytes", path.display())); + } + let mut bytes = Vec::new(); + std::fs::File::open(path) + .map_err(|e| format!("{}: {e}", path.display()))? + .take(limit + 1) + .read_to_end(&mut bytes) + .map_err(|e| format!("{}: {e}", path.display()))?; + if bytes.len() as u64 > limit { + return Err(format!("{}: exceeds {limit} bytes", path.display())); + } + Ok(bytes) } -/// PngSuite: 175 PNGs from a fixed release archive (skipped when already present). -fn fetch_pngsuite() -> Result<(), String> { - let corpus = root().join("corpus"); +fn verify_pngsuite(dir: &Path, manifest: &str) -> Result<(), String> { + let mut expected = BTreeMap::new(); + for line in manifest.lines() { + let Some((hash, name)) = line.split_once(" ") else { return Err("pngsuite: invalid manifest line".into()) }; + if hash.len() != 64 + || !hash.bytes().all(|c| c.is_ascii_hexdigit()) + || name.is_empty() + || name.contains("..") + || !name.bytes().all(|c| c.is_ascii_alphanumeric() || b"._-".contains(&c)) + || expected.insert(name, hash).is_some() + { + return Err("pngsuite: invalid manifest entry".into()); + } + } + if expected.is_empty() { + return Err("pngsuite: empty manifest".into()); + } + let meta = std::fs::symlink_metadata(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + if !meta.is_dir() { + return Err(format!("{}: expected a regular directory", dir.display())); + } + let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut seen = 0; + for entry in entries { + let entry = entry.map_err(|e| format!("{}: {e}", dir.display()))?; + let name = entry.file_name(); + let hash = name.to_str().and_then(|n| expected.get(n)).ok_or_else(|| format!("pngsuite: unexpected file {}", entry.path().display()))?; + let bytes = pngsuite_bytes(&entry.path(), PNGSUITE_MAX_FILE_BYTES)?; + if crate::sha256::hex(&bytes) != **hash { + return Err(format!("pngsuite: {}: sha256 mismatch", entry.path().display())); + } + seen += 1; + } + if seen != expected.len() { + return Err(format!("pngsuite: missing files ({seen} of {})", expected.len())); + } + Ok(()) +} + +struct PngSuiteStage { + path: PathBuf, + keep: bool, +} + +impl PngSuiteStage { + fn new(corpus: &Path) -> Result { + std::fs::create_dir_all(corpus).map_err(|e| format!("create {}: {e}", corpus.display()))?; + for _ in 0..100 { + let id = PNGSUITE_STAGE_ID.fetch_add(1, Ordering::Relaxed); + let path = corpus.join(format!(".pngsuite-{}-{id}", std::process::id())); + match std::fs::create_dir(&path) { + Ok(()) => return Ok(Self { path, keep: false }), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {} + Err(e) => return Err(format!("create {}: {e}", path.display())), + } + } + Err("pngsuite: could not create a staging directory".into()) + } +} + +impl Drop for PngSuiteStage { + fn drop(&mut self) { + if !self.keep { + let _ = std::fs::remove_dir_all(&self.path); + } + } +} + +/// Keep the old cache until the verified replacement is in place. On a failed rollback its +/// backup remains in staging and the error names it, rather than deleting the only copy. +fn install_pngsuite( + dest: &Path, + files: &Path, + backup: &Path, + backup_retained: &mut bool, + mut rename: impl FnMut(&Path, &Path) -> std::io::Result<()>, +) -> Result<(), String> { + let had_cache = match std::fs::symlink_metadata(dest) { + Ok(_) => true, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => false, + Err(e) => return Err(format!("{}: {e}", dest.display())), + }; + if had_cache { + rename(dest, backup).map_err(|e| format!("pngsuite: preserve {}: {e}", dest.display()))?; + *backup_retained = true; + } + if let Err(e) = rename(files, dest) { + if had_cache && let Err(restore) = rename(backup, dest) { + return Err(format!("pngsuite: install failed ({e}); restore failed ({restore}); old cache retained at {}", backup.display())); + } + *backup_retained = false; + return Err(format!("pngsuite: install failed: {e}")); + } + *backup_retained = false; + Ok(()) +} + +fn fetch_pngsuite_in( + corpus: &Path, + archive_hash: &str, + manifest: &str, + download: impl FnOnce(&Path) -> Result<(), String>, + extract: impl FnOnce(&Path, &Path) -> Result<(), String>, + rename: impl FnMut(&Path, &Path) -> std::io::Result<()>, +) -> Result<(), String> { let dest = corpus.join("pngsuite"); - let have = pngsuite_count(&dest); - if have >= 175 { - println!("pngsuite: {have} PNG files in {} already", dest.display()); + if verify_pngsuite(&dest, manifest).is_ok() { + println!("pngsuite: {} already matches its manifest", dest.display()); return Ok(()); } - std::fs::create_dir_all(&dest).map_err(|e| format!("create {}: {e}", dest.display()))?; - let tgz = corpus.join("PngSuite-2017jul19.tgz"); - let mut curl = Command::new("curl"); - curl.args(["-fsSL", "--retry", "3", "-A", USER_AGENT, "-o"]).arg(&tgz).arg(PNGSUITE_URL); - run(curl, &format!("curl {PNGSUITE_URL}"))?; - let mut tar = Command::new("tar"); - tar.arg("-xzf").arg(&tgz).arg("-C").arg(&dest); - run(tar, "tar -xzf PngSuite-2017jul19.tgz")?; - let _ = std::fs::remove_file(&tgz); - let n = pngsuite_count(&dest); - println!("pngsuite: {n} PNG files in {}", dest.display()); - if n == 0 { Err("no PNG files extracted".into()) } else { Ok(()) } + let mut stage = PngSuiteStage::new(corpus)?; + let archive = stage.path.join("archive.tgz"); + download(&archive)?; + let bytes = pngsuite_bytes(&archive, PNGSUITE_MAX_ARCHIVE_BYTES)?; + if crate::sha256::hex(&bytes) != archive_hash { + return Err("pngsuite: archive sha256 mismatch; nothing extracted".into()); + } + let files = stage.path.join("files"); + std::fs::create_dir(&files).map_err(|e| format!("create {}: {e}", files.display()))?; + extract(&archive, &files)?; + verify_pngsuite(&files, manifest)?; + let backup = stage.path.join("previous"); + install_pngsuite(&dest, &files, &backup, &mut stage.keep, rename)?; + println!("pngsuite: verified {}", dest.display()); + Ok(()) +} + +/// Older curl versions cannot enforce --max-filesize for unknown-length responses. Write +/// only the allowed prefix, then probe for excess without spooling that byte to disk. +fn copy_pngsuite_download(reader: &mut impl Read, writer: &mut impl Write) -> std::io::Result<()> { + std::io::copy(&mut reader.take(PNGSUITE_MAX_ARCHIVE_BYTES), writer)?; + let mut extra = [0; 1]; + loop { + match reader.read(&mut extra) { + Ok(0) => return Ok(()), + Ok(_) => return Err(std::io::Error::other(format!("pngsuite: archive exceeds {PNGSUITE_MAX_ARCHIVE_BYTES} bytes"))), + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {} + Err(e) => return Err(e), + } + } +} + +fn finish_pngsuite_download(child: &mut Child, mut reader: impl Read, writer: &mut impl Write) -> Result<(), String> { + let result = copy_pngsuite_download(&mut reader, writer).map_err(|e| format!("pngsuite: download: {e}")).and_then(|()| { + let status = child.wait().map_err(|e| format!("pngsuite: wait for curl: {e}"))?; + if status.success() { Ok(()) } else { Err(format!("pngsuite: curl failed: {status}")) } + }); + if result.is_err() { + let _ = child.kill(); + let _ = child.wait(); + } + result +} + +fn download_pngsuite(mut curl: Command, archive: &Path) -> Result<(), String> { + let mut file = std::fs::File::create(archive).map_err(|e| format!("create {}: {e}", archive.display()))?; + let mut child = curl.stdout(Stdio::piped()).spawn().map_err(|e| format!("pngsuite: start curl: {e}"))?; + let Some(stdout) = child.stdout.take() else { + let _ = child.kill(); + let _ = child.wait(); + return Err("pngsuite: curl stdout unavailable".into()); + }; + finish_pngsuite_download(&mut child, stdout, &mut file) +} + +/// PngSuite: fixed archive checked before tar; every cached file checked on each invocation. +fn fetch_pngsuite() -> Result<(), String> { + fetch_pngsuite_in( + &root().join("corpus"), + PNGSUITE_SHA256, + PNGSUITE_MANIFEST, + |archive| { + let mut curl = Command::new("curl"); + curl.args(["-fsSL", "--retry", "3", "--max-filesize", &PNGSUITE_MAX_ARCHIVE_BYTES.to_string(), "-A", USER_AGENT, "-o", "-", PNGSUITE_URL]); + download_pngsuite(curl, archive) + }, + |archive, files| { + let mut tar = Command::new("tar"); + tar.arg("-xzf").arg(archive).arg("-C").arg(files); + run(tar, "tar -xzf PngSuite-2017jul19.tgz") + }, + |from, to| std::fs::rename(from, to), + ) } /// Files changed relative to `origin/main` (committed, staged or not). @@ -286,4 +472,308 @@ mod tests { fn all_without_update_manifest_only_verifies() { assert!(calls(false, false).iter().all(|(_, u)| !*u)); } + + const FIXTURE_FILES: &[(&str, &[u8])] = &[("a.png", b"alpha"), ("b.png", b"beta"), ("PngSuite.LICENSE", b"fixture licence")]; + const FIXTURE_ARCHIVE: &[u8] = b"owned archive fixture"; + + struct Fixture(PathBuf); + + impl Fixture { + fn new() -> Self { + let id = PNGSUITE_STAGE_ID.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("photocraft-pngsuite-test-{}-{id}", std::process::id())); + std::fs::create_dir(&path).unwrap(); + Self(path) + } + + fn cache(&self) -> PathBuf { + self.0.join("pngsuite") + } + + fn old_cache(&self) { + std::fs::create_dir(self.cache()).unwrap(); + std::fs::write(self.cache().join("old.png"), b"old cache bytes").unwrap(); + } + + fn assert_old_cache(&self) { + assert_eq!(std::fs::read(self.cache().join("old.png")).unwrap(), b"old cache bytes"); + assert_eq!(std::fs::read_dir(&self.0).unwrap().count(), 1, "staging was not cleaned"); + } + } + + impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + fn fixture_manifest() -> String { + FIXTURE_FILES.iter().map(|(name, bytes)| format!("{} {name}\n", crate::sha256::hex(bytes))).collect() + } + + fn fixture_extract(_: &Path, files: &Path) -> Result<(), String> { + for (name, bytes) in FIXTURE_FILES { + std::fs::write(files.join(name), bytes).map_err(|e| e.to_string())?; + } + Ok(()) + } + + fn fixture_download(archive: &Path) -> Result<(), String> { + std::fs::write(archive, FIXTURE_ARCHIVE).map_err(|e| e.to_string()) + } + + fn fixture_fetch(corpus: &Path) -> Result<(), String> { + fetch_pngsuite_in(corpus, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| { + std::fs::rename(from, to) + }) + } + + #[test] + fn pngsuite_cache_checks_bytes_names_and_regular_files() { + let f = Fixture::new(); + std::fs::create_dir(f.cache()).unwrap(); + fixture_extract(Path::new("unused"), &f.cache()).unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).is_ok()); + std::fs::write(f.cache().join("a.png"), b"changed").unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("sha256 mismatch")); + fixture_extract(Path::new("unused"), &f.cache()).unwrap(); + std::fs::remove_file(f.cache().join("a.png")).unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("missing files")); + fixture_extract(Path::new("unused"), &f.cache()).unwrap(); + std::fs::write(f.cache().join("extra.png"), b"extra").unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("unexpected file")); + std::fs::remove_file(f.cache().join("extra.png")).unwrap(); + std::fs::remove_file(f.cache().join("a.png")).unwrap(); + std::fs::create_dir(f.cache().join("a.png")).unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("regular file")); + } + + #[test] + fn pngsuite_valid_cache_needs_no_download_or_extraction() { + let f = Fixture::new(); + fixture_fetch(&f.0).unwrap(); + fetch_pngsuite_in( + &f.0, + "unused", + &fixture_manifest(), + |_| panic!("valid cache downloaded"), + |_, _| panic!("valid cache extracted"), + |_, _| panic!("valid cache renamed"), + ) + .unwrap(); + } + + #[test] + fn pngsuite_repairs_unverified_175_file_cache_and_removes_extras() { + let f = Fixture::new(); + std::fs::create_dir(f.cache()).unwrap(); + for i in 0..175 { + std::fs::write(f.cache().join(format!("fake-{i}.png")), b"not a PNG").unwrap(); + } + fixture_fetch(&f.0).unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).is_ok()); + assert_eq!(std::fs::read_dir(f.cache()).unwrap().count(), FIXTURE_FILES.len()); + assert_eq!(std::fs::read_dir(&f.0).unwrap().count(), 1); + } + + #[test] + fn pngsuite_unexpected_or_truncated_archive_never_reaches_extractor() { + for bad in [b"unexpected".as_slice(), &FIXTURE_ARCHIVE[..4]] { + let f = Fixture::new(); + f.old_cache(); + let e = fetch_pngsuite_in( + &f.0, + &crate::sha256::hex(FIXTURE_ARCHIVE), + &fixture_manifest(), + |a| std::fs::write(a, bad).map_err(|e| e.to_string()), + |_, _| panic!("unverified archive reached extractor"), + |_, _| panic!("unverified archive reached installation"), + ) + .unwrap_err(); + assert!(e.contains("archive sha256 mismatch")); + f.assert_old_cache(); + } + } + + #[test] + fn pngsuite_download_or_extraction_failure_keeps_old_cache() { + let f = Fixture::new(); + f.old_cache(); + assert!( + fetch_pngsuite_in( + &f.0, + "unused", + &fixture_manifest(), + |_| Err("offline".into()), + |_, _| panic!("failed download extracted"), + |_, _| panic!("failed download installed") + ) + .unwrap_err() + .contains("offline") + ); + f.assert_old_cache(); + assert!( + fetch_pngsuite_in( + &f.0, + &crate::sha256::hex(FIXTURE_ARCHIVE), + &fixture_manifest(), + fixture_download, + |_, files| { + std::fs::write(files.join("partial.png"), b"partial").unwrap(); + Err("extract failed".into()) + }, + |_, _| panic!("failed extraction installed") + ) + .unwrap_err() + .contains("extract failed") + ); + f.assert_old_cache(); + for extra in [false, true] { + let e = fetch_pngsuite_in( + &f.0, + &crate::sha256::hex(FIXTURE_ARCHIVE), + &fixture_manifest(), + fixture_download, + |a, files| { + fixture_extract(a, files)?; + if extra { + std::fs::write(files.join("extra.png"), b"extra").unwrap(); + } else { + std::fs::remove_file(files.join("a.png")).unwrap(); + } + Ok(()) + }, + |_, _| panic!("unverified extraction installed"), + ) + .unwrap_err(); + assert!(e.contains(if extra { "unexpected file" } else { "missing files" })); + f.assert_old_cache(); + } + } + + #[test] + fn pngsuite_failed_installation_restores_old_cache() { + for fail_backup in [false, true] { + let f = Fixture::new(); + f.old_cache(); + let e = fetch_pngsuite_in(&f.0, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| { + if (fail_backup && from == f.cache()) || (!fail_backup && from.file_name().is_some_and(|n| n == "files")) { + Err(std::io::Error::other("injected rename failure")) + } else { + std::fs::rename(from, to) + } + }) + .unwrap_err(); + assert!(e.contains("injected rename failure")); + f.assert_old_cache(); + } + } + + #[test] + fn pngsuite_failed_rollback_retains_and_names_the_backup() { + let f = Fixture::new(); + f.old_cache(); + let e = fetch_pngsuite_in(&f.0, &crate::sha256::hex(FIXTURE_ARCHIVE), &fixture_manifest(), fixture_download, fixture_extract, |from, to| { + if from == f.cache() { std::fs::rename(from, to) } else { Err(std::io::Error::other("injected install and rollback failure")) } + }) + .unwrap_err(); + let stage = std::fs::read_dir(&f.0).unwrap().next().unwrap().unwrap().path(); + let backup = stage.join("previous"); + assert!(e.contains(&backup.display().to_string()), "{e}"); + assert_eq!(std::fs::read(backup.join("old.png")).unwrap(), b"old cache bytes"); + std::fs::rename(backup, f.cache()).unwrap(); + } + + #[test] + fn pngsuite_reads_are_bounded_and_malformed_manifests_are_rejected() { + let f = Fixture::new(); + let file = f.0.join("large"); + std::fs::File::create(&file).unwrap().set_len(PNGSUITE_MAX_ARCHIVE_BYTES + 1).unwrap(); + assert!(pngsuite_bytes(&file, PNGSUITE_MAX_ARCHIVE_BYTES).unwrap_err().contains("regular file")); + for malformed in ["", "invalid", "00 a.png", &format!("{} ../outside\n", crate::sha256::hex(b""))] { + assert!(verify_pngsuite(&f.0, malformed).is_err()); + } + } + + #[test] + fn pngsuite_download_copy_accepts_boundary_and_never_writes_excess() { + let limit = PNGSUITE_MAX_ARCHIVE_BYTES as usize; + for len in [limit - 1, limit, limit + 1, limit * 2] { + let data = vec![0xa5; len]; + let mut out = Vec::new(); + let result = copy_pngsuite_download(&mut std::io::Cursor::new(&data), &mut out); + assert_eq!(result.is_ok(), len <= limit); + assert_eq!(out, data[..len.min(limit)]); + } + } + + // The ordinary test invocation returns immediately. Only this test's child command + // enables emission; it stays alive so cleanup must kill and reap that particular child. + #[test] + fn pngsuite_stream_fixture() { + if std::env::var_os("PHOTOCRAFT_PNGSUITE_STREAM_TEST").is_some() { + let mut out = std::io::stdout().lock(); + for _ in 0..256 { + let _ = out.write_all(&[0xa5; 8192]); + } + std::thread::park(); + } + } + + fn stream_child() -> Child { + Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "corpus::tests::pngsuite_stream_fixture", "--nocapture"]) + .env("PHOTOCRAFT_PNGSUITE_STREAM_TEST", "1") + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .unwrap() + } + + #[test] + fn pngsuite_oversize_and_io_failures_kill_and_reap_only_the_download_child() { + struct FailedRead; + impl Read for FailedRead { + fn read(&mut self, _: &mut [u8]) -> std::io::Result { + Err(std::io::Error::other("injected read failure")) + } + } + struct FailedWrite; + impl Write for FailedWrite { + fn write(&mut self, _: &[u8]) -> std::io::Result { + Err(std::io::Error::other("injected write failure")) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + let mut child = stream_child(); + let stdout = child.stdout.take().unwrap(); + let mut out = Vec::new(); + assert!(finish_pngsuite_download(&mut child, stdout, &mut out).unwrap_err().contains("archive exceeds")); + assert_eq!(out.len() as u64, PNGSUITE_MAX_ARCHIVE_BYTES); + assert!(child.try_wait().unwrap().is_some()); + + let mut child = stream_child(); + assert!(child.try_wait().unwrap().is_none()); + assert!(finish_pngsuite_download(&mut child, FailedRead, &mut Vec::new()).unwrap_err().contains("injected read failure")); + assert!(child.try_wait().unwrap().is_some()); + + let mut child = stream_child(); + let stdout = child.stdout.take().unwrap(); + assert!(finish_pngsuite_download(&mut child, stdout, &mut FailedWrite).unwrap_err().contains("injected write failure")); + assert!(child.try_wait().unwrap().is_some()); + } + + #[cfg(unix)] + #[test] + fn pngsuite_cache_rejects_symlinks_even_when_target_bytes_match() { + let f = Fixture::new(); + std::fs::create_dir(f.cache()).unwrap(); + fixture_extract(Path::new("unused"), &f.cache()).unwrap(); + std::fs::write(f.0.join("target"), b"alpha").unwrap(); + std::fs::remove_file(f.cache().join("a.png")).unwrap(); + std::os::unix::fs::symlink(f.0.join("target"), f.cache().join("a.png")).unwrap(); + assert!(verify_pngsuite(&f.cache(), &fixture_manifest()).unwrap_err().contains("regular file")); + } } diff --git a/xtask/src/corpus_pins.rs b/xtask/src/corpus_pins.rs index b8d3770493..917412709a 100644 --- a/xtask/src/corpus_pins.rs +++ b/xtask/src/corpus_pins.rs @@ -40,6 +40,10 @@ pub const ASSET_STORE_TEMPLATE_COMMIT: &str = "f671981ee89d15526285b3cc87b4393d3 pub const AFFINITY_SAMPLES_COMMIT: &str = "96aca6bd86bbe7a677f1cf0630bd832d23943be8"; /// PngSuite (public domain), a fixed release archive. pub const PNGSUITE_URL: &str = "http://www.schaik.com/pngsuite/PngSuite-2017jul19.tgz"; +/// Fixed archive, corroborated by HTTPS records in opencodecs at 6f070029 and rivet-png at +/// 134faad8 (corpus/checksums.json and tests/pngsuite/SHA256SUMS). Check the legacy HTTP +/// download against this committed digest before extraction. +pub const PNGSUITE_SHA256: &str = "0294b244c95a8342c01b00010cf34abdcabc7c6a34fd0fe1bd963917537bfdc8"; /// `corpus/photoshop`: 258 PSDs authored with Photoshop by the photocraft-corpus generator. pub const PHOTOSHOP: PinnedCorpus = PinnedCorpus { From 67e4b8a499b55ef728b1da700005382c45e4677d Mon Sep 17 00:00:00 2001 From: Sean Dellis <1584916+seandellis@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:52:19 -0500 Subject: [PATCH 21/27] =?UTF-8?q?feat:=20Alt+Cancel=20=E2=86=92=20Reset=20?= =?UTF-8?q?for=20opt-in=20command=20dialogs=20(#3871)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update .gitignore and add UI Dialogs documentation - Added .specstory, .private, and .vscode to .gitignore to exclude additional files from version control. - Introduced a new documentation file, ui-dialogs.md, detailing the architecture, rules, and usage of modal and modeless dialogs in the egui shell, mirroring Photoshop's functionality. * Enhance UI Dialogs documentation in ui-dialogs.md - Expanded the section on command routing and dialog types, clarifying the roles of various dialog kinds and their associated modules. - Improved descriptions of routing markers and private keys, ensuring better understanding of the dialog architecture and usage. - Updated the menu command entry details to reflect changes in module organization and functionality. * Implement Alt+Cancel (Reset) functionality for various UI dialogs - Introduced a new module `dialog_alt_reset` to handle the Alt+Cancel behavior, allowing users to reset dialog fields to their original values while keeping the dialog open. - Updated multiple dialog modules to support this feature, including adjustments for color pickers, sizing, and guide layouts. - Added tests to ensure that the reset functionality correctly restores original values and maintains dialog state. - Enhanced documentation to reflect the new Alt+Reset behavior and its implementation details across different dialog types. * Update manual UI testing command to include build timing summary - Modified the `manual-ui-testing` command in `.cargo/config.toml` to include the `--timings` flag, enabling the generation of a build timing summary after compilation and linking. - Refactored the `reset_fields` function in `filter_dialog.rs` for improved readability by consolidating the retrieval of the `spec` variable into a single line, enhancing code clarity. * Layers panel: dragging the vector mask to the Trash asks to delete just the mask (#3119) (#3125) * Layers panel: Shift+Plus/Minus cycle blend modes; effects dropped on the Trash are deleted (#3102) - blend_keys.rs: Shift+Plus / Shift+Minus step to the next / previous blend mode, wrapping: the brush's mode with the Brush or Pencil (tools.setBrush), the Gradient's with the Gradient tool, otherwise the active layer's (layer.setProps; groups include Pass Through, the Background is left alone). One undo step per press. - panels.rs: an effect row dragged onto the footer's Delete button deletes that effect, the "Effects" row clears the whole style (layer.layerStyle.clear). - engine: layer.layerStyle.clear takes an optional "effect" index ("Delete Effect" step); bad indices and layers return Err. - scorecard UI-217-14 -> done (its wheel, footer drop and thumbnail checker parts were already on main via #1961, #965). Co-Authored-By: Claude Opus 5.5 * Layers panel: Shift+Plus/Minus step the blend mode on a German layout too (#3102) On layouts with a key of its own for `+` (German, Italian, Spanish) Shift+`+` types `*`, which egui has no key for, so egui-winit reports the physical key: `]` on German. That matched Shift+] (Increase Brush Hardness) before the blend keys ever saw it. Shift+`-` types `_` and arrives as the physical `/`. shortcuts::layout_symbols now turns such a fallback press (Shift, the key equal to its physical key, not a digit, next to the text `*` or `_`) into `+` / `-` before any shortcut is dispatched. A US Shift+] still arrives as `}` and keeps raising the hardness; US Shift+8 (`*`) stays a digit for the opacity keys. Co-Authored-By: Claude Opus 5.5 * Shortcuts: Ö / # are the brush keys on a German layout (#3109) Photoshop's German shortcuts step the brush size with Ö / # and the hardness with Shift+Ö / Shift+#, since [ and ] need AltGr there (and that press carries Alt on the Mac and Windows, which the shortcut match rejects). egui can't name ö or #, so they arrived as the physical ; and \ and matched nothing; Shift+# types ' and arrived as Quote. shortcuts::layout_symbols (from #3102) now reads a table of typed symbol and Shift: ö / Ö -> [, # / ' -> ], next to * -> + and _ -> -. It runs at the top of shortcuts::handle (unless a text field has focus), so Liquify's and the Magnetic Lasso's [ ] get the German keys too. Only presses whose key egui took from the physical key or from that very text change, and never digits. Co-Authored-By: Claude Opus 5.5 * Layers panel: a mask dragged onto the Trash asks to apply or delete it (#3113) A drag that started on a layer's mask thumbnail dragged the whole row, and dropping it on the Trash deleted the layer. Now a drag from the pixel mask thumbnail (where it was drawn last frame) drags the mask: it shows a "Layer Mask" ghost and starts neither a layer reorder nor a cross-document transfer. Dropped on the Trash it opens Photoshop's prompt "Apply mask to layer before removing?" with Apply (layer.layerMask.apply, greyed out where it can't run), Delete (layer.layerMask.delete) and Cancel. Clicking the Trash still deletes the targeted mask at once (#2197). The prompt (mask_trash_prompt.rs) is an ordinary dialog: ui.dialog.confirm applies, ui.dialog.set {"choice": "delete"} + confirm deletes, cancel keeps. Two new strings in all 16 catalogs. Co-Authored-By: Claude Opus 5.5 * Layers panel: dragging the vector mask to the Trash asks to delete just the mask (#3119) A drag that started on a row's vector mask thumbnail picked up the whole layer row, so dropping it on the Trash deleted the layer. The mask drag from #3113 now covers both thumbnails: a vector mask dropped on the Trash opens Photoshop's "Delete the vector mask?" (OK / Cancel), and OK runs layer.vectorMask.delete. Clicks on the thumbnail are unchanged. Co-Authored-By: Claude Opus 5.5 * Brush keys: layout table plus bracket-position fallback for every layout (#3109) Co-Authored-By: Claude Opus 5.5 * layout_keys: drop unused field, fmt Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Interface: an Enable Animations preference (#3273) (#3276) Preferences › Interface › Enable animations (interface.enableAnimations, on by default). Off, prefs_ui::sync_animations sets egui's animation_time to 0 and scroll_animation to none in both the dark and light styles, so menus, popups and collapsing sections change at once and scroll areas jump. It is re-applied each frame, so a theme change keeps it. Translated in all 16 catalogs; the scorecard row UI-217-10 goes to partial. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Edit > Copy takes a targeted layer mask, so masks travel between layers and documents (#2928) (#3267) * automation: export layers, data sets and CSVs through the granted roots (#3216) (#3230) * automation: doc.place reads a file through the read root (#2810) file.placeEmbedded takes an ambient path, so automation refuses it and an agent could not place a file into the open document at all. doc.place resolves the path beneath the read root the way doc.open does and hands the bytes it already read to the public file_cmds::place_bytes, so the existing cap-std rules stay the only path rules: no new scope logic, and the ambient command keeps its refusal. Trusted-local callers still run the command. * automation: export layers, data sets and CSVs through the roots (#3216) file.export.layersToFiles and file.export.dataSetsAsFiles take an ambient dir, file.import.variableDataSets an ambient path, so all three are refused in automation even for files inside the granted roots, and an agent could not export a single file or a data set at all. Both halves already existed: the engine writes bytes it has just encoded (save_doc is encode + write_file, and the data-set exporter encodes with photocraft_io::export before writing), and the automation side already holds a write capability — AuthorizedWorkspace::export_sequence creates the directory, refuses to overwrite, accepts only single-component names and removes every file it created when a step fails, which is what Render Video uses. So each command body moves behind the same sink callback render_video_with takes: the command keeps writing the bytes it is handed to its dir, the new methods write the very same bytes through the root, and the two implementations cannot drift. --------- Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 * Type: fall back to installed Khmer, Lao and Burmese fonts (#218) (#3259) Text in these scripts drew .notdef boxes although the OS had a font for them. Detection was never the problem: the system font scan loads them, and asking for one by name shapes correctly. The fallback stack is a list of family names intersected with the installed fonts, and it had no entry for any of the three, so an installed Khmer, Lao or Burmese font could not be reached from a Latin-only face. - photocraft-text: KHMER_FAMILIES, LAO_FAMILIES and MYANMAR_FAMILIES join fallback_candidates next to THAI_FAMILIES; the web build's served-font scripts gain Khmr, Laoo and Mymr (with their ranges and languages). - photocraft-ui-egui: the UI's lazy loader had one non-CJK slot (Thai). It becomes a LazyScript slot table, so the three scripts are data rather than a third and fourth copy of the same state machine. - Tests assert detection and selection separately, so a future failure names its layer, and skip when a machine lacks the script's font. Co-authored-by: Raccoon <235524086+RaccoonPie-314@users.noreply.github.com> Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 * Curves: pencil mode, Smooth and Edit points (#3088) (#3108) * Curves: pencil mode, Smooth and Edit points (#3088) The Curves editor (Image > Adjustments dialog and the adjustment layer's Properties panel) gains Photoshop's "Edit points to modify the curve" and "Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn curve is a 256-level map per channel, evaluated piecewise-linearly. - doc: Adjustment::Curves gains `pencil` (serde default), with curve_map / curve_map_outputs helpers. - compose: curve_map_lut / curve_lut_mode for drawn curves. - engine: curves params take `pencil` and `smooth`; channels accept 256-level maps; switching modes draws the spline into maps or fits up to 16 points to the drawing. Eyedroppers fit points first. - io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read as padding, so pencil curves from Photoshop were misparsed; they are now read and written as 256-byte maps, and smart-filter Curves use `Mpng`. - scorecard UI-217-15 note updated. Co-Authored-By: Claude Opus 5.5 * Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * fix(io): smooth PSD gradients no longer overshoot their stops (#3028) (#3285) The Catmull-Rom tangents used for Smoothness are derived from the neighbouring stops, so a segment between two equal-coloured stops (a flat plateau) picked up a non-zero tangent and dipped below its own colour. In the Gradient Map from #3028 the 27%-53% plateau (#1c1c1c) rendered about 19/255 too dark. Bound each interpolated channel to the range of the segment's two end colours. Smoothness still bends the ramp, but never invents a colour outside the segment, so plateaus stay exactly flat. The regression test uses the four stop positions and grey values from the issue (synthetic, no artwork) and fails without the clamp. * Codecs: decode PNM straight into the budgeted output buffer (#2938) (#3291) Limits::max_alloc budgets the decoded pixel buffer, but the PNM decoder collected one u32 per sample first (4x an 8-bit image, plus a u16 copy for 16-bit) and PFM built a Vec beside the output bytes. Samples are now rescaled and written straight into the native-endian output buffer (reserved fallibly), so the only header-sized allocation is the one Limits::check already charged. Co-authored-by: Claude Opus 5.5 * Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110) (#3115) * Curves: pencil mode, Smooth and Edit points (#3088) The Curves editor (Image > Adjustments dialog and the adjustment layer's Properties panel) gains Photoshop's "Edit points to modify the curve" and "Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn curve is a 256-level map per channel, evaluated piecewise-linearly. - doc: Adjustment::Curves gains `pencil` (serde default), with curve_map / curve_map_outputs helpers. - compose: curve_map_lut / curve_lut_mode for drawn curves. - engine: curves params take `pencil` and `smooth`; channels accept 256-level maps; switching modes draws the spline into maps or fits up to 16 points to the drawing. Eyedroppers fit points first. - io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read as padding, so pencil curves from Photoshop were misparsed; they are now read and written as 256-byte maps, and smart-filter Curves use `Mpng`. - scorecard UI-217-15 note updated. Co-Authored-By: Claude Opus 5.5 * Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110) The Set Black/Neutral Gray/White Point pickers existed only in the Image > Adjustments > Curves dialog (#873). The Properties editor of a Curves adjustment layer now has them too (layer_picker.rs): with one armed, a click on the image runs one layer.setAdjustment with the new `eyedropper` param, which samples the image below the layer (the layer hidden, like its Properties histogram) at the Eyedropper's Sample Size, so a repeated click keeps the curve and the edit is one undoable, recordable step agents can send directly. Esc, another tool or another layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it. Scorecard UI-217-15 is done. Co-Authored-By: Claude Opus 5.5 * Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Format: admit the declared .pcraft canvas before loading it (#2935) (#3295) * Format: admit the declared .pcraft canvas before loading it (#2935) A .pcraft manifest's canvas size was copied into the Document unchecked, so a few bytes of JSON could declare a 300000x300000 (or u32::MAX) canvas that the CPU reduction, thumbnails and display textures then work against in proportion to its area. The loader now refuses an empty canvas, a side above 300000 (Photoshop's PSB limit, as for PSD and file.new) and an area above 4 gigapixels, right after reading the manifest and before any tile or blob is decoded. Both limits are LoadOptions fields so trusted callers can change them. Co-Authored-By: Claude Opus 5.5 * Format: admit every canvas the app can create on .pcraft load (#2935) The default pixel budget was 2^32 (65536 x 65536), but file.new, Canvas Size, Image Size with Resample off and PSB import all reach 300000 x 300000, so a document a user could create and save would not reopen. The default is now the full 300000^2 PSB canvas; the side limit still refuses anything larger, and callers handling untrusted files can lower either budget. Loading stays proportional to the file: layers keep only the tiles the bundle holds, so a huge declared canvas allocates nothing for its area. A compile-time assert in document_preset_cmds ties file.new's MAX_DIMENSION to the loader's defaults. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 * fix(format): save the target file name in native bundle manifests (#3312) (#3319) * Levels: black, gray and white point eyedroppers (#3116) (#3128) * Curves: pencil mode, Smooth and Edit points (#3088) The Curves editor (Image > Adjustments dialog and the adjustment layer's Properties panel) gains Photoshop's "Edit points to modify the curve" and "Draw to modify the curve" buttons plus Smooth, with tooltips. A drawn curve is a 256-level map per channel, evaluated piecewise-linearly. - doc: Adjustment::Curves gains `pencil` (serde default), with curve_map / curve_map_outputs helpers. - compose: curve_map_lut / curve_lut_mode for drawn curves. - engine: curves params take `pencil` and `smooth`; channels accept 256-level maps; switching modes draws the spline into maps or fits up to 16 points to the drawing. Eyedroppers fit points first. - io: the PSD `curv` map flag (first byte, psd-tools `is_map`) was read as padding, so pencil curves from Photoshop were misparsed; they are now read and written as 256-byte maps, and smart-filter Curves use `Mpng`. - scorecard UI-217-15 note updated. Co-Authored-By: Claude Opus 5.5 * Properties: Curves adjustment layers get the black/gray/white eyedroppers (#3110) The Set Black/Neutral Gray/White Point pickers existed only in the Image > Adjustments > Curves dialog (#873). The Properties editor of a Curves adjustment layer now has them too (layer_picker.rs): with one armed, a click on the image runs one layer.setAdjustment with the new `eyedropper` param, which samples the image below the layer (the layer hidden, like its Properties histogram) at the Eyedropper's Sample Size, so a repeated click keeps the curve and the edit is one undoable, recordable step agents can send directly. Esc, another tool or another layer disarms it; ui.pointer and ui.inspect (`layerPicker`) cover it. Scorecard UI-217-15 is done. Co-Authored-By: Claude Opus 5.5 * Levels: black, gray and white point eyedroppers (#3116) Image > Adjustments > Levels and a Levels adjustment layer's Properties panel get Photoshop's Set Black/Neutral Gray/White Point eyedroppers. Black/white move each channel's input black/white to the sample, gray sets each channel's midtone gamma so the sample turns neutral at its luma (Grayscale: composite input, no gray). The engine takes the same `eyedropper` param as Curves on image.adjustments.levels, layer.newAdjustmentLayer.levels and layer.setAdjustment. On a smart object, image.adjustments.levels/curves with `eyedropper` now record the resolved adjustment as the smart filter instead of the raw sample, which re-rendered as an identity (#3127). Co-Authored-By: Claude Opus 5.5 * Merge upstream/main: Curves in new compose/gpu HDR tests gets the pencil field (#3088) Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Puppet Warp: the mesh follows the object outline (#3105) (#3117) The mesh was every square grid cell touching the layer's opaque pixels: a staircase up to a cell outside the shape, and shapes closer than about a cell shared cells, so pinning one dragged the other along. build_mesh now traces the outline of the opaque region grown (or shrunk) by Expansion from a signed distance field, simplifies it (Douglas-Peucker, corners kept), splits its sides to the Density spacing, fills the inside with a triangular lattice and triangulates the points (Bowyer-Watson Delaunay, outline sides recovered by midpoint splits), keeping the triangles inside the outline. Separate shapes get separate meshes, so an unpinned one stays put, as in Photoshop. Vertices are numbered in reverse Cuthill-McKee order to keep the solver's banded factor narrow, and the density's spacing counts the grown region so a large Expansion on a small layer no longer builds tens of thousands of vertices. Scorecard TOOL-214-6 done; docs/gaps.md G7 updated. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Clone Source: W/H link and overlay blend modes (#3298) (#3310) The Clone Source panel gets Photoshop's W/H chain (Maintain aspect ratio): with it on, a new W or H scales the other by the same factor. cloneSource.set takes "linked" and cloneSource.list reports it. The overlay's Darken, Lighten and Difference modes were stored but never drawn. draw_clone_preview now blends the source with the composite under the brush (clone_source::overlay_blend), so Difference goes black where source and image line up. Co-authored-by: Claude Opus 5.5 * Vanishing Point: on-canvas plane editor with Create/Edit Plane, grid and paste (#3122) (#3133) Filter > Vanishing Point now opens a dialog mode on the canvas instead of the generic parameter dialog: - Create Plane (C): four clicks, in any order, make a plane (corners are normalised clockwise from the top-left, the order paste expects). - Edit Plane (V): drag corner nodes, or edge nodes to slide an edge along the plane's own perspective; click a plane to select it, Delete removes it. - The plane grid is drawn at the Grid Size, in the accent colour when the plane is valid, warning yellow when its perspective is doubtful and red (outline only) when the quad isn't convex (VpPlane::check). - With an image on the clipboard the selected plane previews it pasted in perspective; drag to move it, Width sets its size, New Layer renders it on a new layer. OK / Enter runs filter.vanishingPoint with the planes and the paste as one history step; the control channel drives the same session through {"ui": {...}}. The engine now rejects a drawn plane whose corners don't form a convex quad (bow-tie, dented) instead of warping along it. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas * Let `[` shrink the brush to 1 px (#3229) (#3332) `[` steps the brush size down by 25 %, rounded: from 2 px that is 1.6, which rounds back to 2, so the key never reached 1 px and pixel artists had to type the size. `]` already grows by at least a pixel; `[` now shrinks by at least a pixel the same way, still never below 1 px. * MCP: command_batch says it is not atomic, and the docs explain recovery (#3338) Agents read the tool description, not docs/mcp.md, and the command_batch description only said it stops at the first error. It now also says the steps run in order and that earlier successful steps stay applied: there is no rollback. docs/mcp.md gives the stop_on_error default and a recovery recipe: inspect with doc_inspect, undo with edit.undo once per history entry (undo does not reverse files written to disk), or experiment on a copy made with image.duplicate or history.newDocument. A test checks the listed description. Claude-Session: https://claude.ai/code/session_01BSB7fMQUccqFQekksDGWpo Co-authored-by: Claude Opus 5.5 --------- Co-authored-by: Valentin Klein Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas Co-authored-by: Vikram Bhaskaran Co-authored-by: Gert Pellin - GPE Co-authored-by: switch87 <8284204+switch87@users.noreply.github.com> Co-authored-by: RaccoonPie-314 Co-authored-by: Raccoon <235524086+RaccoonPie-314@users.noreply.github.com> Co-authored-by: Sir Starch Co-authored-by: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Co-authored-by: myangaa1 <58806636+myangaa1@users.noreply.github.com> Co-authored-by: KnottyDyes <47539659+KnottyDyes@users.noreply.github.com> --- crates/ui-egui/src/adjust_dialog.rs | 51 +++-- crates/ui-egui/src/adjust_editors.rs | 8 + crates/ui-egui/src/color_picker_ui.rs | 21 ++ crates/ui-egui/src/dialog_alt_reset.rs | 93 +++++++++ crates/ui-egui/src/dialogs.rs | 5 +- crates/ui-egui/src/fill_ui.rs | 23 +- crates/ui-egui/src/fill_ui_tests.rs | 16 ++ crates/ui-egui/src/filter_dialog.rs | 123 +++++++---- crates/ui-egui/src/guide_layout_ui.rs | 27 +++ crates/ui-egui/src/guide_layout_ui/tests.rs | 17 ++ crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/sizing.rs | 38 ++++ crates/ui-egui/src/variables_ui.rs | 23 ++ crates/ui-egui/src/view_cmds.rs | 35 +++- docs/ui-dialogs.md | 219 ++++++++++++++++++++ 15 files changed, 646 insertions(+), 54 deletions(-) create mode 100644 crates/ui-egui/src/dialog_alt_reset.rs create mode 100644 docs/ui-dialogs.md diff --git a/crates/ui-egui/src/adjust_dialog.rs b/crates/ui-egui/src/adjust_dialog.rs index 7c97835354..a9235881ea 100644 --- a/crates/ui-egui/src/adjust_dialog.rs +++ b/crates/ui-egui/src/adjust_dialog.rs @@ -134,19 +134,33 @@ fn dialog_mem(kind: &str) -> egui::Id { egui::Id::new(("adjust-dialog", kind)) } -/// Whether holding Alt turns the dialog's Cancel into Reset. Measured in Photoshop 25.4 for Color -/// Balance; other kinds keep Cancel until checked against Photoshop. +/// Whether holding Alt turns Cancel into Reset for this adjustment dialog (every kind). pub fn resets(fields: &Map) -> bool { - fields.get("__adjust").and_then(Value::as_str) == Some("colorBalance") + owns(fields) } -/// Alt+Cancel (Reset): back to the defaults Photoshop's Reset restores, dialog stays open. -pub fn reset(ctx: &egui::Context, fields: &mut Map) { - if !resets(fields) { +/// Alt+Cancel (Reset): engine defaults for the kind; Color Balance keeps Preserve Luminosity and +/// shows Midtones (Photoshop 25.4). Dialog stays open. +pub fn reset(app: &PhotocraftApp, ctx: &egui::Context, fields: &mut Map) { + if !owns(fields) { return; } + let kind = fields.get("__adjust").and_then(Value::as_str).unwrap_or_default().to_string(); + let mode = app.session.active().map_or(photocraft_doc::ColorMode::Rgb, |s| s.doc.mode); + // Color Balance: Photoshop keeps the user's Preserve Luminosity across Reset. + let preserve = if kind == "colorBalance" { fields.get("preserveLuminosity").cloned() } else { None }; + let Ok(defaults) = photocraft_engine::adjust_params::default_for(&kind, mode) else { return }; + let values = photocraft_engine::adjust_params::to_params(&defaults); + // Replace parameter keys; keep private `__` chrome (preview, command, …). + fields.retain(|k, _| k.starts_with("__")); + if let Value::Object(v) = values { + fields.extend(v); + } + if let Some(p) = preserve { + fields.insert("preserveLuminosity".into(), p); + } let mut values = crate::filter_dialog::params_of(fields); - adjust_editors::reset_color_balance(ctx, dialog_mem("colorBalance"), &mut values); + adjust_editors::reset_adjustment_editor(ctx, &kind, dialog_mem(&kind), &mut values); if let Value::Object(v) = values { fields.extend(v); } @@ -390,16 +404,29 @@ mod tests { assert!(h.state().ui.dialogs.is_empty(), "Cancel closes"); } - /// Alt+Cancel is Reset only where it was checked against Photoshop. + /// Every adjustment kind opts into Alt+Reset; Levels restores engine defaults and stays open. #[test] - fn other_adjustment_dialogs_keep_cancel_under_alt() { + fn levels_alt_reset_restores_defaults() { use egui_kittest::kittest::Queryable; let mut h = Harness::builder().with_size(egui::vec2(1200.0, 900.0)).build_ui_state(|ui, app| crate::dialogs::show(app, ui.ctx()), app_with_image()); PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]); - open(h.state_mut(), "image.adjustments.levels").unwrap(); - h.event(egui::Event::ModifiersChanged(egui::Modifiers::ALT)); - h.run_steps(3); + let id = open(h.state_mut(), "image.adjustments.levels").unwrap(); + h.state_mut().ui.dialog_mut(id).unwrap().fields.insert("inBlack".into(), json!(30)); + h.state_mut().ui.dialog_mut(id).unwrap().fields.insert("gamma".into(), json!(1.3)); + h.run_steps(2); assert!(h.query_by_label("Cancel").is_some() && h.query_by_label("Reset").is_none()); + h.event(egui::Event::ModifiersChanged(egui::Modifiers::ALT)); + h.run_steps(2); + let reset = h.get_by_label("Reset").rect(); + assert!(h.query_by_label("Cancel").is_none()); + let click = + |pressed| egui::Event::PointerButton { pos: reset.center(), button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::ALT }; + input(&mut h, egui::Modifiers::ALT, &[egui::Event::PointerMoved(reset.center()), click(true), click(false)]); + assert!(h.state().ui.dialogs.iter().any(|d| d.id == id), "Reset keeps the dialog open"); + let defaults = + photocraft_engine::adjust_params::to_params(&photocraft_engine::adjust_params::default_for("levels", photocraft_doc::ColorMode::Rgb).unwrap()); + assert_eq!(field(&h, id, "inBlack"), defaults["inBlack"]); + assert_eq!(field(&h, id, "gamma"), defaults["gamma"]); } fn sample_params(kind: &str) -> Value { diff --git a/crates/ui-egui/src/adjust_editors.rs b/crates/ui-egui/src/adjust_editors.rs index 4d25d822c6..84fab70b4b 100644 --- a/crates/ui-egui/src/adjust_editors.rs +++ b/crates/ui-egui/src/adjust_editors.rs @@ -1019,6 +1019,14 @@ pub fn reset_color_balance(ctx: &egui::Context, mem: egui::Id, v: &mut Value) { ctx.data_mut(|d| d.insert_temp(color_balance_tone_id(mem), 1usize)); } +/// Kind-specific editor memory after Alt+Reset (or Properties Reset to defaults). Color Balance +/// shows Midtones; other kinds have no dialog-local temp to clear yet. +pub fn reset_adjustment_editor(ctx: &egui::Context, kind: &str, mem: egui::Id, v: &mut Value) { + if kind == "colorBalance" { + reset_color_balance(ctx, mem, v); + } +} + fn color_balance(ui: &mut egui::Ui, v: &mut Value, cx: &EditorCx) -> Edit { let tone_id = color_balance_tone_id(cx.mem); let mut tone_ix: usize = ui.data(|d| d.get_temp(tone_id)).unwrap_or(1); diff --git a/crates/ui-egui/src/color_picker_ui.rs b/crates/ui-egui/src/color_picker_ui.rs index cfa7183a59..e9729eab67 100644 --- a/crates/ui-egui/src/color_picker_ui.rs +++ b/crates/ui-egui/src/color_picker_ui.rs @@ -298,6 +298,15 @@ pub fn owns(f: &Map) -> bool { f.contains_key("__colorPicker") } +/// Alt+Cancel (Reset): restore the colour from when the picker opened (same as clicking Current). +pub fn reset(f: &mut Map) { + if !owns(f) { + return; + } + let orig = f.get("__orig").and_then(Value::as_str).and_then(parse_hex).unwrap_or([0.0; 3]); + set_rgb(f, orig, Keep::Nothing); +} + /// The Color Picker, when it is the top dialog (another dialog opened over it takes the input). pub fn top(app: &PhotocraftApp) -> Option { app.ui.dialogs.last().filter(|d| owns(&d.fields)).map(|d| d.id) @@ -833,6 +842,18 @@ mod tests { assert_eq!(hex([h.state().session.tools.foreground[0], h.state().session.tools.foreground[1], h.state().session.tools.foreground[2]]), "#ff8800"); } + #[test] + fn alt_reset_restores_orig_color() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.session.tools.foreground = [0.2, 0.4, 0.6, 1.0]; + let id = open(&mut app, "foreground"); + let f = &mut app.ui.dialog_mut(id).unwrap().fields; + f.insert("color".into(), json!("#ff0000")); + reset(f); + assert_eq!(f["color"], json!(hex([0.2, 0.4, 0.6]))); + assert_eq!(f["__orig"], json!(hex([0.2, 0.4, 0.6]))); + } + #[test] fn lab_and_cmyk_fields_take_multi_digit_values() { let base = parse_hex("#336699").unwrap(); diff --git a/crates/ui-egui/src/dialog_alt_reset.rs b/crates/ui-egui/src/dialog_alt_reset.rs new file mode 100644 index 0000000000..71e52ac8f6 --- /dev/null +++ b/crates/ui-egui/src/dialog_alt_reset.rs @@ -0,0 +1,93 @@ +//! Alt+Cancel → Reset for command dialogs that opt in. +//! +//! Holding Alt in the dialog chrome relabels Cancel as Reset and, on click, restores the dialog's +//! fields without closing it (Photoshop). Each marker implements its own `reset`; this module only +//! dispatches. Esc stays Cancel (`Modifiers::NONE` in the frame). Opt out with `"__altReset": false`. + +use serde_json::{Map, Value}; + +use crate::PhotocraftApp; + +/// Whether holding Alt turns Cancel into Reset for this dialog's fields. +pub fn supported(fields: &Map) -> bool { + if fields.get("__altReset") == Some(&Value::Bool(false)) { + return false; + } + match_arm(fields).is_some() +} + +/// Apply Reset in place. No-op when the dialog has not opted in. +pub fn apply(app: &mut PhotocraftApp, ctx: &egui::Context, fields: &mut Map) { + if fields.get("__altReset") == Some(&Value::Bool(false)) { + return; + } + // Dispatch order matches `dialogs::show` so `__adjust` wins over `__filter`. + if crate::guide_layout_ui::owns(fields) { + crate::guide_layout_ui::reset(app, fields); + } else if crate::fill_ui::owns(fields) { + crate::fill_ui::reset(app, fields); + } else if crate::variables_ui::owns(fields) { + crate::variables_ui::reset(fields); + } else if crate::color_picker_ui::owns(fields) { + crate::color_picker_ui::reset(fields); + } else if fields.contains_key("__sizing") { + crate::sizing::reset(fields); + } else if crate::adjust_dialog::owns(fields) { + crate::adjust_dialog::reset(app, ctx, fields); + } else if fields.contains_key("__filter") { + crate::filter_dialog::reset_fields(app, fields); + } else if fields.contains_key("__form") { + crate::view_cmds::reset_form(fields); + } +} + +/// First matching opt-in arm, if any (same order as [`apply`]). +fn match_arm(fields: &Map) -> Option<&'static str> { + if crate::guide_layout_ui::owns(fields) { + Some("guideLayout") + } else if crate::fill_ui::owns(fields) { + Some("fill") + } else if crate::variables_ui::owns(fields) && fields.contains_key("__variablesOrig") { + Some("variables") + } else if crate::color_picker_ui::owns(fields) { + Some("colorPicker") + } else if fields.contains_key("__sizing") { + Some("sizing") + } else if crate::adjust_dialog::owns(fields) { + Some("adjust") + } else if fields.contains_key("__filter") { + Some("filter") + } else if fields.contains_key("__form") && fields.contains_key("__formOrig") { + Some("form") + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn alt_reset_false_opts_out() { + let mut f = Map::new(); + f.insert("__adjust".into(), json!("levels")); + f.insert("__altReset".into(), json!(false)); + assert!(!supported(&f)); + } + + #[test] + fn adjust_marker_opts_in() { + let mut f = Map::new(); + f.insert("__adjust".into(), json!("levels")); + assert!(supported(&f)); + } + + #[test] + fn bare_confirm_does_not_opt_in() { + let mut f = Map::new(); + f.insert("__command".into(), json!("edit.purge")); + assert!(!supported(&f)); + } +} diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2765d0e2a5..1573d5ec31 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -310,7 +310,8 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { let can_ok = crate::crop_presets::can_confirm(app, &fields); let ok = DialogButton::new(ButtonRole::Default, ok_label, 84.0).enabled(can_ok); // Photoshop: holding Alt turns Cancel into Reset (the dialog stays open). - let reset = ui.input(|i| i.modifiers.alt) && crate::adjust_dialog::resets(&fields); + // Dialogs opt in via `dialog_alt_reset::supported` (per marker). + let reset = ui.input(|i| i.modifiers.alt) && crate::dialog_alt_reset::supported(&fields); let cancel_label = if reset { tl!("Reset") } else if d.kind == DialogKind::NewDocument { @@ -337,7 +338,7 @@ pub fn show(app: &mut PhotocraftApp, ctx: &egui::Context) { dialog_buttons(ui, &[ok, cancel]) }; match clicked { - Some(ButtonRole::Cancel) if reset => crate::adjust_dialog::reset(ui.ctx(), &mut fields), + Some(ButtonRole::Cancel) if reset => crate::dialog_alt_reset::apply(app, ui.ctx(), &mut fields), Some(ButtonRole::Cancel) => outcome = Some(false), Some(ButtonRole::Apply) => apply_requested = true, Some(_) => outcome = Some(true), diff --git a/crates/ui-egui/src/fill_ui.rs b/crates/ui-egui/src/fill_ui.rs index c44c4b5774..faa917f039 100644 --- a/crates/ui-egui/src/fill_ui.rs +++ b/crates/ui-egui/src/fill_ui.rs @@ -53,8 +53,8 @@ fn valid(app: &PhotocraftApp, key: &str, v: &Value) -> bool { } } -/// The dialog's fields: Photoshop's defaults, overridden by the last choices. -pub fn fields(app: &PhotocraftApp) -> Map { +/// Photoshop's Fill defaults (no last-used prefs). Used by open and Alt+Reset. +pub fn default_fields(app: &PhotocraftApp) -> Map { let mut f = Map::new(); f.insert(MARK.into(), json!(true)); f.insert("__command".into(), json!(COMMAND)); @@ -68,6 +68,12 @@ pub fn fields(app: &PhotocraftApp) -> Map { f.insert("mode".into(), json!("normal")); f.insert("opacity".into(), json!(100.0)); f.insert("preserveTransparency".into(), json!(false)); + f +} + +/// The dialog's fields: Photoshop's defaults, overridden by the last choices. +pub fn fields(app: &PhotocraftApp) -> Map { + let mut f = default_fields(app); if let Some(Value::Object(saved)) = app.session.prefs().dialogs.get(COMMAND) { for k in KEYS { if let Some(v) = saved.get(k).filter(|v| valid(app, k, v)) { @@ -78,6 +84,19 @@ pub fn fields(app: &PhotocraftApp) -> Map { f } +/// Alt+Cancel (Reset): Photoshop Fill defaults, not last-used prefs. Dialog stays open. +pub fn reset(app: &PhotocraftApp, fields: &mut Map) { + if !owns(fields) { + return; + } + let defaults = default_fields(app); + for k in KEYS { + if let Some(v) = defaults.get(k) { + fields.insert(k.into(), v.clone()); + } + } +} + /// Open the Fill dialog; returns its id. pub fn open(app: &mut PhotocraftApp) -> u64 { let f = fields(app); diff --git a/crates/ui-egui/src/fill_ui_tests.rs b/crates/ui-egui/src/fill_ui_tests.rs index aa5f2772eb..5d353127f8 100644 --- a/crates/ui-egui/src/fill_ui_tests.rs +++ b/crates/ui-egui/src/fill_ui_tests.rs @@ -74,6 +74,22 @@ fn mode_opacity_and_preserve_transparency() { assert!(close(px(&app, 5, 5), [0.5, 0.5, 0.5, 1.0])); } +#[test] +fn alt_reset_restores_photoshop_defaults_not_last_used() { + let mut app = app(); + // Remember a non-default choice, then open (which restores it) and Reset. + fill(&mut app, json!({"contents": "white", "opacity": 40})).unwrap(); + let id = open(&mut app); + assert_eq!(app.ui.dialog_mut(id).unwrap().fields["opacity"].as_f64(), Some(40.0)); + let mut fields = app.ui.dialog_mut(id).unwrap().fields.clone(); + reset(&app, &mut fields); + app.ui.dialog_mut(id).unwrap().fields = fields; + let f = &app.ui.dialog_mut(id).unwrap().fields; + assert_eq!(f["opacity"].as_f64(), Some(100.0)); + assert_eq!(f["contents"], json!("foreground")); + assert!(app.ui.dialogs.iter().any(|d| d.id == id)); +} + #[test] fn choices_persist_across_restarts() { let mut app = app(); diff --git a/crates/ui-egui/src/filter_dialog.rs b/crates/ui-egui/src/filter_dialog.rs index 5f2f853fda..29fc436e0f 100644 --- a/crates/ui-egui/src/filter_dialog.rs +++ b/crates/ui-egui/src/filter_dialog.rs @@ -327,6 +327,70 @@ pub(crate) fn remember(app: &mut PhotocraftApp, command: &str, fields: &Map Option { + Some(match &p.kind { + Kind::Range { default, .. } if command == "image.mode.indexedColor" && p.key == "colors" => json!(default.round().clamp(2.0, 256.0) as u32), + Kind::Range { default, .. } => json!(default), + Kind::Choice { options, default } => { + let initial = default.as_deref().or_else(|| options.first().map(String::as_str)).unwrap_or(""); + json!(initial) + } + Kind::Bool(default) => json!(default), + Kind::Int { default } => json!(default), + Kind::Text => json!(""), + Kind::Document => json!(-1), + // Identity kernel: 1 in the centre. + Kind::Grid(n) => json!((0..*n).map(|i| i64::from(i == *n / 2)).collect::>()), + Kind::Color(Some(rgb)) => json!(crate::color_picker_ui::hex(*rgb)), + Kind::Color(None) => return None, + // Colour inputs come from the current swatches, so the proxy preview matches the result. + Kind::Json if p.key == "foreground" => json!(app.session.tools.foreground), + Kind::Json if p.key == "background" => json!(app.session.tools.background), + Kind::Json => return None, + }) +} + +/// Command-specific open defaults (ruler straighten, warp from layer, …), not remembered prefs. +fn apply_command_defaults(app: &PhotocraftApp, command: &str, fields: &mut Map) { + if command == "image.rotation.arbitrary" { + straighten_defaults(app, fields); + } + if command == "type.warpText" { + warp_defaults(app, fields); + } + // Photoshop's Pattern Name dialog starts from the name the pattern would get anyway. + if command == "edit.definePattern" { + fields.insert("name".into(), json!(photocraft_engine::pattern_cmds::default_name(&app.session))); + } + if command == "layer.layerStyle.globalLight" { + global_light_defaults(app, fields); + } + if command == "view.proofSetup" { + proof_setup_defaults(app, fields); + } +} + +/// Alt+Cancel (Reset): schema defaults + command hooks, without last-used remembered prefs. +pub fn reset_fields(app: &PhotocraftApp, fields: &mut Map) { + if !fields.contains_key("__filter") || fields.contains_key("__adjust") { + return; + } + let Some(command) = fields.get("__command").and_then(Value::as_str).map(str::to_owned) else { return }; + let spec = + fields.get("__spec").and_then(Value::as_str).map(str::to_owned).or_else(|| photocraft_engine::commands::find(&command).map(|c| c.params.to_owned())); + let Some(spec) = spec else { return }; + // Drop user params; keep private `__` chrome (and fixed keys outside the schema for `__spec`). + let schema_keys: std::collections::HashSet = parse_spec(&spec).into_iter().map(|p| p.key).collect(); + fields.retain(|k, _| k.starts_with("__") || !schema_keys.contains(k)); + for p in parse_spec(&spec) { + if let Some(v) = default_value(app, &command, &p) { + fields.insert(p.key, v); + } + } + apply_command_defaults(app, &command, fields); +} + pub fn open(app: &mut PhotocraftApp, command: &str) -> Option { let spec = photocraft_engine::commands::find(command)?; let mut fields = Map::new(); @@ -338,45 +402,12 @@ pub fn open(app: &mut PhotocraftApp, command: &str) -> Option { fields.insert("__preview".into(), json!(true)); } for p in parse_spec(spec.params) { - let v = match &p.kind { - Kind::Range { default, .. } if command == "image.mode.indexedColor" && p.key == "colors" => json!(default.round().clamp(2.0, 256.0) as u32), - Kind::Range { default, .. } => json!(default), - Kind::Choice { options, default } => { - let initial = default.as_deref().or_else(|| options.first().map(String::as_str)).unwrap_or(""); - json!(initial) - } - Kind::Bool(default) => json!(default), - Kind::Int { default } => json!(default), - Kind::Text => json!(""), - Kind::Document => json!(-1), - // Identity kernel: 1 in the centre. - Kind::Grid(n) => json!((0..*n).map(|i| i64::from(i == *n / 2)).collect::>()), - Kind::Color(Some(rgb)) => json!(crate::color_picker_ui::hex(*rgb)), - Kind::Color(None) => continue, - // Colour inputs come from the current swatches, so the proxy preview matches the result. - Kind::Json if p.key == "foreground" => json!(app.session.tools.foreground), - Kind::Json if p.key == "background" => json!(app.session.tools.background), - Kind::Json => continue, - }; - fields.insert(p.key, v); + if let Some(v) = default_value(app, command, &p) { + fields.insert(p.key, v); + } } restore_remembered(app, command, spec.params, &mut fields); - if command == "image.rotation.arbitrary" { - straighten_defaults(app, &mut fields); - } - if command == "type.warpText" { - warp_defaults(app, &mut fields); - } - // Photoshop's Pattern Name dialog starts from the name the pattern would get anyway. - if command == "edit.definePattern" { - fields.insert("name".into(), json!(photocraft_engine::pattern_cmds::default_name(&app.session))); - } - if command == "layer.layerStyle.globalLight" { - global_light_defaults(app, &mut fields); - } - if command == "view.proofSetup" { - proof_setup_defaults(app, &mut fields); - } + apply_command_defaults(app, command, &mut fields); if parse_spec(spec.params).iter().any(|p| p.kind == Kind::Document) { // The document picker lists every open document (params refer to them by index). let names: Vec = app.session.documents().iter().map(|d| d.doc.name.clone()).collect(); @@ -818,6 +849,24 @@ mod tests { assert_eq!(fields["radius"], json!(2.0)); } + #[test] + fn alt_reset_restores_schema_defaults_not_remembered() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default()); + app.session.prefs.edit(|prefs| { + prefs.dialogs.insert("filter.blur.gaussianBlur".into(), json!({"radius": 11.0})); + }); + let id = open(&mut app, "filter.blur.gaussianBlur").unwrap(); + assert_eq!(app.ui.dialog_mut(id).unwrap().fields["radius"], json!(11.0)); + let mut fields = app.ui.dialog_mut(id).unwrap().fields.clone(); + reset_fields(&app, &mut fields); + app.ui.dialog_mut(id).unwrap().fields = fields; + let f = &app.ui.dialog_mut(id).unwrap().fields; + // Registry default for gaussian blur radius is 1. + assert_eq!(f["radius"], json!(1.0)); + assert!(f.contains_key("__filter")); + assert!(app.ui.dialogs.iter().any(|d| d.id == id)); + } + #[test] fn remembering_dialogs_never_persists_paths_or_document_indices() { let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default()); diff --git a/crates/ui-egui/src/guide_layout_ui.rs b/crates/ui-egui/src/guide_layout_ui.rs index d6c5c9fdbe..9117baa829 100644 --- a/crates/ui-egui/src/guide_layout_ui.rs +++ b/crates/ui-egui/src/guide_layout_ui.rs @@ -31,6 +31,33 @@ pub fn owns(f: &Map) -> bool { f.contains_key(MARK) } +/// Alt+Cancel (Reset): factory New Guide Layout values, not the last-used remembered layout. +pub fn reset(app: &PhotocraftApp, f: &mut Map) { + if !owns(f) { + return; + } + let Value::Object(defaults) = crate::view_cmds::default_guide_layout() else { return }; + // Drop layout params and interactive toggles; keep dialog chrome (`__command`, …). + f.retain(|k, _| { + k.starts_with("__") + && !matches!( + k.as_str(), + "__preset" | "__columnsEnabled" | "__rowsEnabled" | "__marginEnabled" | "__previewGuides" | "__previewParams" | "__previewSource" + ) + }); + f.extend(defaults); + f.insert("color".into(), json!(app.session.prefs().guides_grid_and_slices.guide_color)); + f.insert("target".into(), json!("document")); + f.insert("__preset".into(), json!("custom")); + f.insert("width".into(), Value::Null); + f.insert("height".into(), Value::Null); + let margin = f.get("margin").cloned().unwrap_or(json!(0)); + if !margin.is_array() { + f.insert("margin".into(), json!([margin.clone(), margin.clone(), margin.clone(), margin])); + } + f.insert("__preview".into(), json!(true)); +} + fn on(f: &Map, key: &str, fallback: bool) -> bool { f.get(key).and_then(Value::as_bool).unwrap_or(fallback) } diff --git a/crates/ui-egui/src/guide_layout_ui/tests.rs b/crates/ui-egui/src/guide_layout_ui/tests.rs index 17abf3e381..6ac36cb1d8 100644 --- a/crates/ui-egui/src/guide_layout_ui/tests.rs +++ b/crates/ui-egui/src/guide_layout_ui/tests.rs @@ -84,6 +84,23 @@ fn app() -> PhotocraftApp { PhotocraftApp::new(session, crate::Services::default()) } +#[test] +fn alt_reset_restores_factory_layout() { + let mut app = app(); + let id = open(&mut app); + let mut fields = app.ui.dialog_mut(id).unwrap().fields.clone(); + fields.insert("columns".into(), json!(3)); + fields.insert("gutter".into(), json!(99)); + fields.insert("__preset".into(), json!("twoColumn")); + reset(&app, &mut fields); + app.ui.dialog_mut(id).unwrap().fields = fields; + let f = &app.ui.dialog_mut(id).unwrap().fields; + assert_eq!(f["columns"], json!(8)); + assert_eq!(f["gutter"], json!(20)); + assert_eq!(f["__preset"], json!("custom")); + assert!(app.ui.dialogs.iter().any(|d| d.id == id)); +} + fn draw(app: &mut PhotocraftApp, id: u64) { let mut f = app.ui.dialog_mut(id).unwrap().fields.clone(); let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d6309afccc..08e3464dde 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -61,6 +61,7 @@ pub mod crop_size; pub mod crop_straighten; pub mod crop_ui; pub mod delete_layer_prompt; +pub mod dialog_alt_reset; pub mod dialog_blend_ui; pub mod dialogs; pub mod direct_select; diff --git a/crates/ui-egui/src/sizing.rs b/crates/ui-egui/src/sizing.rs index a0fb7557b0..f4a9f54f06 100644 --- a/crates/ui-egui/src/sizing.rs +++ b/crates/ui-egui/src/sizing.rs @@ -46,6 +46,28 @@ pub fn is_sizing(command: &str) -> bool { matches!(command, "image.imageSize" | "image.canvasSize") } +/// Alt+Cancel (Reset): restore the dimensions and options from when the dialog opened. +pub fn reset(f: &mut Map) { + if !f.contains_key("__sizing") { + return; + } + let w = num(f, "__origW"); + let h = num(f, "__origH"); + f.insert("width".into(), json!(w)); + f.insert("height".into(), json!(h)); + if f.get("__command").and_then(Value::as_str) == Some("image.imageSize") { + f.insert("resolution".into(), json!(num(f, "__origRes"))); + f.insert("__constrain".into(), json!(true)); + if let Some(r) = f.get("__resample").cloned() { + f.insert("resample".into(), r); + } + } else { + f.insert("relative".into(), json!(false)); + f.insert("anchor".into(), json!("center")); + f.insert("extensionColor".into(), json!("background")); + } +} + pub fn open(app: &mut PhotocraftApp, command: &str) -> Option { let unit = pref_unit(app.session.prefs().units_and_rulers.rulers); let resample = pref_resample(app.session.prefs().general.image_interpolation); @@ -443,6 +465,22 @@ mod tests { assert_eq!(app.session.active().unwrap().doc.size, photocraft_doc::Size::new(120, 50)); } + #[test] + fn alt_reset_restores_open_dimensions() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + app.session.execute("file.new", json!({"width": 200, "height": 100, "resolution": 72})).unwrap(); + let id = open(&mut app, "image.imageSize").unwrap(); + let d = app.ui.dialog_mut(id).unwrap(); + d.fields.insert("width".into(), json!(50)); + d.fields.insert("height".into(), json!(25)); + d.fields.insert("resolution".into(), json!(300)); + d.fields.insert("__constrain".into(), json!(false)); + reset(&mut d.fields); + assert_eq!((num(&d.fields, "width"), num(&d.fields, "height"), num(&d.fields, "resolution")), (200.0, 100.0, 72.0)); + assert_eq!(d.fields.get("__constrain"), Some(&json!(true))); + assert!(app.ui.dialogs.iter().any(|x| x.id == id), "reset leaves the dialog open"); + } + /// The dialog starts on Preferences › General › Image Interpolation: Bicubic Automatic opens /// as Automatic and the others as themselves (Bicubic Sharper used to open as Lanczos). #[test] diff --git a/crates/ui-egui/src/variables_ui.rs b/crates/ui-egui/src/variables_ui.rs index fc10df402f..a4700cddad 100644 --- a/crates/ui-egui/src/variables_ui.rs +++ b/crates/ui-egui/src/variables_ui.rs @@ -17,6 +17,16 @@ pub fn owns(fields: &Map) -> bool { fields.contains_key("__variables") } +/// Alt+Cancel (Reset): restore `__variables` from the open-time snapshot. +pub fn reset(fields: &mut Map) { + if !owns(fields) { + return; + } + if let Some(orig) = fields.get("__variablesOrig").cloned() { + fields.insert("__variables".into(), orig); + } +} + /// Layers available to bind, top to bottom: (id, display name, is-text). fn layer_options(app: &PhotocraftApp) -> Vec<(u64, String, bool)> { let Some(st) = app.session.active() else { return Vec::new() }; @@ -31,6 +41,8 @@ pub fn menu(app: &mut PhotocraftApp, id: &str, params: &Value) -> Option Option Value { +/// Factory New Guide Layout values (Alt+Reset and a fresh ViewOptions). +pub(crate) fn default_guide_layout() -> Value { json!({"columns": 8, "gutter": 20, "rows": 0, "rowGutter": 0, "margin": 0, "centerColumns": false, "clearExisting": false}) } @@ -933,9 +934,22 @@ fn form(app: &mut PhotocraftApp, command: &str, label: &str, fields: Value, choi if let Value::Object(m) = fields { f.extend(m); } + // Snapshot user-facing params at open so Alt+Reset can restore them. + let orig: Map = f.iter().filter(|(k, _)| !k.starts_with("__")).map(|(k, v)| (k.clone(), v.clone())).collect(); + f.insert("__formOrig".into(), Value::Object(orig)); app.ui.open_dialog(DialogKind::Command, f) } +/// Alt+Cancel (Reset): restore the `__form` params captured at open. +pub fn reset_form(f: &mut Map) { + if !f.contains_key("__form") { + return; + } + let Some(Value::Object(orig)) = f.get("__formOrig").cloned() else { return }; + f.retain(|k, _| k.starts_with("__")); + f.extend(orig); +} + fn label_of(key: &str) -> String { let mut out = String::new(); for (i, c) in key.chars().enumerate() { @@ -971,6 +985,25 @@ mod label_tests { } } +#[cfg(test)] +mod form_reset_tests { + use super::*; + use serde_json::json; + + #[test] + fn alt_reset_restores_form_orig() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + let id = form(&mut app, "test.form", "Test", json!({"radius": 5.0, "on": true}), json!({})); + let f = &mut app.ui.dialog_mut(id).unwrap().fields; + f.insert("radius".into(), json!(99.0)); + f.insert("on".into(), json!(false)); + reset_form(f); + assert_eq!(f["radius"], json!(5.0)); + assert_eq!(f["on"], json!(true)); + assert!(f.contains_key("__formOrig")); + } +} + /// Body of a `__form` dialog: text fields, number fields, checkboxes and `__choices` dropdowns. pub fn form_body(ui: &mut egui::Ui, f: &mut Map) { let choices = f.get("__choices").cloned().unwrap_or(Value::Null); diff --git a/docs/ui-dialogs.md b/docs/ui-dialogs.md new file mode 100644 index 0000000000..30db03fd1b --- /dev/null +++ b/docs/ui-dialogs.md @@ -0,0 +1,219 @@ +# Dialogs + +Modal and modeless parameter windows in the egui shell. They mirror Photoshop: no dimmed backdrop, +canvas pan/zoom under an open dialog, Tab cycles text and number fields only, Esc cancels, Enter +confirms when OK is enabled. Automation drives the same `fields` map as the UI (`ui.dialog.*` in +[`control-protocol.md`](control-protocol.md)). + +## Architecture + +| Piece | Location | Role | +|---|---|---| +| Frame, routing, OK/Cancel/Esc, stacking | `crates/ui-egui/src/dialogs.rs` | Single hub for every `UiState::dialogs` entry | +| Alt+Cancel → Reset (opt-in) | `crates/ui-egui/src/dialog_alt_reset.rs` | Relabels Cancel while Alt is held; dispatches per-marker `reset` | +| Tab-through-fields | `crates/ui-egui/src/field_tab.rs` | Photoshop-style Tab / ⇧Tab (tests in same file) | +| State | `crates/ui-egui/src/state.rs` | `Dialog`, `DialogKind`, `open_dialog` | +| Button row | `crates/ui-egui/src/widgets.rs` | `dialog_buttons`, `focus_first_field`; `number_edit` registers for Tab | +| Menu / command entry | `dialogs::open_command_dialog`, `menus.rs` | Picks a dedicated module, schema dialog, or bare confirm | +| Filters | `filter_dialog.rs` | Schema from engine params, live preview | +| Adjustments | `adjust_dialog.rs` | Shared editors with Properties; preview via `adjust_preview` | +| Generic forms | `view_cmds.rs` | `__form` grids for simple command params | +| Dedicated bodies | See **Command routing** below | `body` + usually `open` / `confirm` | + +**Routing.** Non-command kinds use `DialogKind` alone. `DialogKind::Command` bodies are chosen by +marker keys on `fields` (`owns()` or `contains_key("__export")`, etc.); **first match in +`dialogs::show` wins**, so new markers must not collide with an earlier arm. `show`, `confirm`, and +`cancel` share the same dispatch table—wire all three when adding a dialog. + +**Field contract.** `Dialog.fields` is a JSON map: command parameters agents read and write, plus +private keys prefixed with `__`. Never use `__` for user-facing or engine params. + +### `DialogKind` (top-level stack) + +| Kind | Body module | Typical entry | +|---|---|---| +| `NewDocument` | `new_doc_ui.rs` | File › New, `file.new` with no size params | +| `About` | `dialogs.rs` (About / Contributors / Models tabs) | Help › About PhotoCraft; `systemInfo: true` → System Info variant | +| `Command` | Routed—see below | Most menu commands with parameters | +| `LayerStyle` | `layer_style.rs` | Layer › Layer Style… | +| `Error` | inline in `dialogs.rs` | `message` field only (e.g. move lock, locked transform) | + +### Command routing (`DialogKind::Command`) + +Dispatch order matches `dialogs::show` in `dialogs.rs` (check earlier rows before adding a marker). + +| Match | Module | Role | +|---|---|---| +| `__guideLayout` | `guide_layout_ui.rs` | View › New Guide Layout… | +| `__fill` | `fill_ui.rs` | Edit › Fill… | +| `__patternFill` | `pattern_fill_ui.rs` | Pattern Fill layer / New Fill Layer › Pattern… | +| `__stroke` | `stroke_ui.rs` | Edit › Stroke… | +| `__createShape` | `shape_dialog.rs` | Click-with-shape-tool creation (rectangle, ellipse, …) | +| `__deleteLayer` | `delete_layer_prompt.rs` | Layers panel trash confirmation | +| `__newLayer` | `new_layer_ui.rs` | Layer › New › Layer… (⇧⌘N) | +| `__rasterize` | `rasterize_prompt.rs` | Rasterize-before-paint prompt | +| `__resetAllTools` | `tool_reset.rs` | Reset All Tools confirmation | +| `__cropPresetNew` / `__cropPresetDelete` | `crop_presets.rs` | New / delete crop preset | +| `__variables` | `variables_ui.rs` | Image › Variables (Define, Data Sets, Apply) | +| `__web` / `__print` | `file_ui.rs` | Save for Web, Print | +| `__colorPicker` | `color_picker_ui.rs` | Foreground/background, nested field/command pickers (see nested keys below) | +| `__colorRange` | `color_range_ui.rs` | Select › Color Range… | +| `__prefsui` | `prefs_ui.rs` | Edit › Preferences… and related preset/import subdialogs | +| `__export` | `export_dialog.rs` | Export As (document or layer) | +| `__sizing` | `sizing.rs` | Image › Image Size… / Canvas Size… | +| `__adjust` | `adjust_dialog.rs` | Image › Adjustments… (also sets `__filter` + `__preview` for live preview) | +| `__filter` | `filter_dialog.rs` (+ `select_mask_view.rs` when `__command` is `select.refineEdge`) | Filter Gallery commands, Edit › Color Settings…, Select and Mask, and other schema dialogs | +| `__form` | `view_cmds.rs` | Simple param grids (e.g. slice options, many Image › Apply Image-style commands) | +| (none of the above) | — | **Bare confirm**: title + OK/Cancel only; OK runs `__command` with fields via generic `confirm` | + +**Menu shortcut.** `dialogs::open_command_dialog` opens guide layout, stroke, and color range +directly; then tries `adjust_dialog::open`, then `filter_dialog::open`; otherwise a bare confirm +with `__command` + `__label`. Other dedicated modules are opened from `menus.rs`, tools, or panel +actions with their marker already set. + +### Shared private keys + +| Key | Meaning | +|---|---| +| `__command` | Engine command id run on OK (bare confirm, filters, sizing, …) | +| `__label` | Menu/catalog label for the title | +| `__document` | Target document id (validate staleness) | +| `__dialog` / `__field` | Nested color picker: parent dialog id and field name to write on OK | +| `__params` | Extra command params for command-targeted color pickers | +| `__preview` | Live canvas preview enabled (filters and adjustments) | +| `__choices` | Dropdown options for `__form` dialogs | +| `__filter` | Filter-schema body (`filter_dialog`); also set on adjustments for preview plumbing | +| `__adjust` | Image › Adjustments editor kind (`adjust_dialog`) | +| `__form` | Generic `view_cmds::form_body` | +| `__formOrig` | Snapshot of non-`__` params at open; Alt+Reset restores it for `__form` dialogs | +| `__variablesOrig` | Snapshot of `__variables` at open; Alt+Reset restores it | +| `__altReset` | Set to `false` to opt a dialog out of Alt+Cancel → Reset | +| `__export` | Export As | +| `__sizing` | Image Size / Canvas Size | + +### Routing markers (dedicated `Command` bodies) + +| Key | Module | +|---|---| +| `__guideLayout` | `guide_layout_ui` | +| `__fill` | `fill_ui` | +| `__patternFill` | `pattern_fill_ui` | +| `__stroke` | `stroke_ui` | +| `__createShape` | `shape_dialog` | +| `__deleteLayer` | `delete_layer_prompt` | +| `__newLayer` | `new_layer_ui` | +| `__rasterize` | `rasterize_prompt` | +| `__resetAllTools` | `tool_reset` | +| `__cropPresetNew`, `__cropPresetDelete` | `crop_presets` | +| `__variables` | `variables_ui` | +| `__web`, `__print` | `file_ui` | +| `__colorPicker` | `color_picker_ui` | +| `__colorRange` | `color_range_ui` | +| `__prefsui` | `prefs_ui` (string sub-kind: `prefs`, `presetsIO`, …) | + +### Nested color pickers (`__colorPicker` on the stack) + +Same hub entry as standalone pickers; `color_picker_ui::confirm` branches on extra keys: + +| Extra key | Behaviour | +|---|---| +| `__layerStyleColor` | Layer Style child; OK writes effect colour on parent `LayerStyle` dialog | +| `__textStyleColor` | Character/paragraph style colour (type panels) | +| `__shapeFill` | Shape layer solid fill via `shape.edit` | +| `__command` = `layer.fill.solidColor.set` | Solid Color fill layer (no `__shapeFill`) | +| `__dialog` + `__field` | OK updates one field on a parent command dialog (e.g. Fill › Color…) | + +## Parallel modal systems (not the main stack) + +| System | Location | When | +|---|---|---| +| Unsaved changes | `discard_ui.rs` | Close, Revert, Close Others/All, Exit—Save / Don't Save / Cancel | +| Shell dialog | `control.rs`, `ui.inspect` id `"shell"` | Workspace, custom pixel aspect, show-extras options | +| Camera Raw | `camera_raw_ui.rs` | Separate OS window on desktop; see control-protocol § Camera Raw | +| Error message | `DialogKind::Error` | e.g. `move_lock`—message + OK | +| Nested pickers | `color_picker_ui` (+ markers in **Nested color pickers**) | Stack on parent; parent confirm refused while child open | + +## Rules + +### Architecture and commands + +1. **Thin UI, engine command on OK.** Run `app.run` or a dedicated `*_ui::confirm`; one history step unless the command itself batches. +2. **Register in the hub.** Add `owns()` (or a stable marker), `body()`, `open()` if needed, and matching arms in `dialogs::show`, `confirm`, and `cancel` (and `open_command_dialog` when menu-driven). +3. **Extend existing paths first.** Filters → `filter_dialog`; adjustments → `adjust_dialog`; simple param grids → `__form`; avoid a second modal stack unless product needs Camera Raw–level UX. +4. **`fields` is the automation contract.** Anything agents must drive lives in named fields, not only in local egui state. +5. **Remember last-used values** after successful OK where Photoshop does (`filter_dialog::remember`, prefs `dialogs["…"]`, module-specific helpers). + +### Chrome and layout + +6. **Use `dialog_buttons`** for OK / Cancel / Apply—platform order via `ButtonRole` (macOS default action in the corner). +7. **Respect width rules** in `dialogs::show` unless `file_ui::dialog_width`, color picker, or kind-specific mins apply (typical 380–440; Layer Style / export / picker up to 600; New Document 800). +8. **Scroll long bodies** inside the content (`DIALOG_CHROME` for filters); title and button row stay visible. +9. **Transparent backdrop**—no dimming; previews are judged at true contrast. +10. **Position:** first open centered; then persist offset (`place_id` per command or kind); title bar drag; clamp on screen. +11. **Stacking:** only the **top** dialog is interactive; non-top instances render disabled; raise the live dialog if an older modal steals focus. + +### Keyboard and focus + +12. **Tab / ⇧Tab:** text and number fields only. Frame calls `field_tab::begin` / `end`; each field calls `field_tab::register` (`number_edit` does this; custom `TextEdit` must register manually). +13. **Do not rely on egui default Tab** through sliders, dropdowns, checkboxes, or preset cards. +14. **Open on first value:** `first_field_focused` + `widgets::focus_first_field` for value-first dialogs (most filters; Image/Canvas Size; not Select and Mask view keys). +15. **Select-all on focus** when reached via Tab or field arrows (`field_tab::select_all`; see `value_field`, `new_doc_ui`). +16. **Enter** confirms when OK is enabled (`can_ok`); exceptions must be explicit (`guide_layout_ui`, color picker). +17. **Esc** on topmost dialog with no popup open → Cancel. **Click outside does not dismiss** (canvas may still pan/zoom via `free_pointer_over`, `pan_delta`, `free_press`). Esc ignores Alt (it does not become Reset). +18. **Alt** turns Cancel into **Reset** when the dialog opts in via `dialog_alt_reset::supported` (dialog stays open). Opt-in markers: `__guideLayout`, `__fill`, `__variables`, `__colorPicker`, `__sizing`, `__adjust` (all kinds), `__filter`, `__form`. Each module implements its own `reset`; set `"__altReset": false` to disable. + +### Canvas and preview + +19. **Live preview** for filters/adjustments uses `filter_preview` / `adjust_preview`; `cancel()` clears preview and related shell state. +20. **Under-dialog canvas** respects dialog rects and open popups; Hand / Space / middle-button pan on free canvas only. + +### Labels and i18n + +21. **Titles:** command dialogs use `__label` (catalogued; trailing `…` stripped in chrome); fixed kinds use `dialogs::title` + `tl!`. +22. **User-visible strings:** `tl!("…")`; command labels from the menu catalog where possible. + +### Confirm / cancel safety + +23. **Child dialogs first**—refuse parent confirm if a nested color picker is open; cancel parent closes children where implemented. +24. **Validate before close** when params can be inconsistent (`guide_layout_ui::validate` in `confirm`). +25. **Disable OK** when required input is missing (`crop_presets::can_confirm`, Preferences Apply when unchanged). +26. **Preferences over control:** whole-dialog OK/Apply may be refused; safe single prefs via `prefs.set` (see control-protocol). + +### Testing + +27. **egui_kittest:** open dialog, `run_steps`, key presses, assert `fields` or accessibility roles (`field_tab` tests are the reference harness). +28. **Tab-order regressions:** add tests when a dialog gains multiple text/number fields. +29. **Filter preview:** `dialogs.rs` tests for preview persistence across background jobs. + +### Anti-patterns + +30. No panics in `body` / `confirm` on bad input—errors to status or validation. +31. No hard-coded colours or radii—`theme::Tokens` and `widgets.rs`. +32. Do not skip `confirm` dispatch—Enter and `ui.dialog.confirm` use it. +33. Do not expect click-outside-to-close on `egui::Modal`. +34. Do not register sliders or dropdowns for `field_tab` unless product explicitly changes Photoshop parity. + +## Adding a command dialog (checklist) + +1. Choose path: see **Command routing** (`adjust_dialog`, `filter_dialog`, `__form`, `__sizing`, …, or new `*_ui` with a unique marker). +2. `open`: build `fields` (`__command`, `__label`, params, marker); `app.ui.open_dialog(DialogKind::Command, f)`. +3. `body(app, ui, &mut fields)`—mutate `fields` only; use shared widgets. +4. `confirm` / `cancel` arms in `dialogs.rs` (or delegate to module `confirm`). +5. Tab: register every text/number field; use `value_field` / `number_edit` where possible. +6. **Alt+Reset (optional):** implement `reset` on the module and add an arm in `dialog_alt_reset` (same dispatch order as `dialogs::show`). Snapshot open values under a `__*Orig` key when Reset should restore “as opened” rather than factory defaults. +7. Tests: at least open + confirm or Tab sequence if non-trivial; if Alt+Reset is supported, assert Reset restores fields and leaves the dialog open. +8. If menu-visible: command already in engine; menu id opens dialog via existing menu wiring. + +## Known gaps + +- **Tab registration** for plain `TextEdit` is manual; generic `__form` string fields may not join `field_tab` unless wired. +- **“Dialog module”** is any UI routed from `dialogs::show`, not only files named `*dialog*`. +- **Numeric field parity** (units in expressions, scrubby labels) is tracked in [`ui-parity.md`](ui-parity.md) (Navigation / numeric fields), not duplicated here. + +## Revision history + +| Date | Change | +|---|---| +| 2026-10-11 | Initial doc from dialog codebase audit | +| 2026-10-11 | Document all `DialogKind` values and `Command` routing markers | +| 2026-10-11 | Alt+Cancel → Reset hub (`dialog_alt_reset`); opt-in markers | From b3309e27d736a6de9f865390292ccfa2f86a3871 Mon Sep 17 00:00:00 2001 From: Ufuk Efe Date: Mon, 12 Oct 2026 18:01:46 +0300 Subject: [PATCH 22/27] i18n(tr): add Turkish UI translation catalog (#3809) Add Turkish (tr) translation catalog covering 100% of measured UI strings (1,727/1,727 keys, 3,731 catalog entries). - Register Turkish language in crates/ui-egui/src/i18n/mod.rs with complete_menus: true and plural_none - Add tr.tsv translation catalog - Add negotiation and coverage tests in system.rs, mod.rs and i18n_coverage.rs - Add MIT OR Apache-2.0 notice in LICENSE-translations.txt - Update docs/localization-parity.md Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- ATTRIBUTION.md | 1 + .../ui-egui/src/i18n/LICENSE-translations.txt | 5 +- crates/ui-egui/src/i18n/mod.rs | 7 +- crates/ui-egui/src/i18n/system.rs | 1 + crates/ui-egui/src/i18n/tr.tsv | 3784 +++++++++++++++++ docs/localization-parity.md | 4 +- xtask/src/i18n_coverage.rs | 1 + 7 files changed, 3800 insertions(+), 3 deletions(-) create mode 100644 crates/ui-egui/src/i18n/tr.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 9fc1fa2625..7aece035b7 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -20,6 +20,7 @@ next to them. | `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/cs.tsv` | Czech UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | +| `crates/ui-egui/src/i18n/tr.tsv` | Turkish UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) | | `assets/fonts/JetBrainsMono-Regular.ttf` | JetBrains Mono 2.305 (numeric font) | The JetBrains Mono Project Authors | | SIL OFL 1.1, [`assets/fonts/OFL-JetBrainsMono.txt`](assets/fonts/OFL-JetBrainsMono.txt) | | `assets/icons/*.svg` except `slice-knife.svg`, `sampler.svg`, `artboard.svg`, `perspective-crop.svg`, `frame-placeholder.svg`, `type-mask.svg`, `type-mask-vertical.svg`, `clip-below.svg`, `clip-release.svg`, `layer-mask.svg`, `adjustment-layer.svg`, `brush-replace.svg`, `eraser-background.svg`, `eraser-magic.svg`, `lasso-magnetic.svg`, `direct-select.svg`, `convert-point.svg`, `pen-add-anchor.svg`, `pen-delete-anchor.svg`, `pen-freeform.svg`, `pen-curvature.svg`, `bandage-sparkle.svg`, `painting-symmetry.svg`, `symmetry-*.svg` and `marquee-{row,column}.svg` (113 files; `star-filled.svg` is `star.svg` with a solid fill) | Lucide icons | Lucide Icons and Contributors | (`icons/.svg`; `align-*` are now named `text-align-*` upstream) | ISC, [`assets/icons/LICENSE-lucide.txt`](assets/icons/LICENSE-lucide.txt) | diff --git a/crates/ui-egui/src/i18n/LICENSE-translations.txt b/crates/ui-egui/src/i18n/LICENSE-translations.txt index bf2f8caaae..eae278e4dc 100644 --- a/crates/ui-egui/src/i18n/LICENSE-translations.txt +++ b/crates/ui-egui/src/i18n/LICENSE-translations.txt @@ -1,4 +1,4 @@ -PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch and Czech UI translation catalogs are +PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch, Czech and Turkish UI translation catalogs are licensed under MIT OR Apache-2.0, at your option. The full license texts are in ../../../../LICENSE-MIT and ../../../../LICENSE-APACHE. @@ -22,3 +22,6 @@ PhotoCraft's English labels; no proprietary translation resources were used. Czech translations: PhotoCraft contributors, original translations of PhotoCraft's English labels; no proprietary translation resources were used. + +Turkish translations: PhotoCraft contributors, original translations of +PhotoCraft's English labels; no proprietary translation resources were used. diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index ecf21f46d0..be8cb5c15e 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -141,7 +141,7 @@ fn inflate(source: &[u8]) -> Result { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 18] = [ +pub static LANGUAGES: [LangInfo; 19] = [ LangInfo { code: "en", name: "English", source: b"", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: catalog!("ja"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, LangInfo { code: "zh-hans", name: "简体中文", source: catalog!("zh-hans"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, @@ -164,6 +164,8 @@ pub static LANGUAGES: [LangInfo; 18] = [ // Dutch; `nl-NL` and `nl-BE` locales both resolve here. LangInfo { code: "nl", name: "Nederlands", source: catalog!("nl"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() }, LangInfo { code: "it", name: "Italiano", source: catalog!("it"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() }, + // Turkish; `tr`, `tr-TR` and `tr_TR` locales resolve here. + LangInfo { code: "tr", name: "Türkçe", source: catalog!("tr"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, // Vietnamese; `vi-VN` resolves here. LangInfo { code: "vi", name: "Tiếng Việt", source: catalog!("vi"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, ]; @@ -418,6 +420,9 @@ mod tests { assert_eq!(lang_from_tag("it-CH"), Lang::from_code("it")); assert_eq!(lang_from_tag("nl_NL.UTF-8"), Lang::from_code("nl")); assert_eq!(lang_from_tag("nl-BE"), Lang::from_code("nl")); + assert_eq!(lang_from_tag("tr_TR.UTF-8"), Lang::from_code("tr")); + assert_eq!(lang_from_tag("tr-TR"), Lang::from_code("tr")); + assert_eq!(lang_from_tag("tr"), Lang::from_code("tr")); // Traditional Chinese: by region, by script, and with a region after the script. assert_eq!(lang_from_tag("zh_TW.UTF-8"), Some(ZH())); assert_eq!(lang_from_tag("zh-TW"), Some(ZH())); diff --git a/crates/ui-egui/src/i18n/system.rs b/crates/ui-egui/src/i18n/system.rs index 4fb0404f66..68afea8d85 100644 --- a/crates/ui-egui/src/i18n/system.rs +++ b/crates/ui-egui/src/i18n/system.rs @@ -96,6 +96,7 @@ mod tests { (vec!["pt-PT"], "pt-br"), (vec!["it-IT", "en-US"], "it"), (vec!["nl-BE", "fr-BE"], "nl"), + (vec!["tr-TR", "en-US"], "tr"), (vec![], "en"), ] { with_system_tags(&tags, || assert_eq!(system_lang().code(), expected)); diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv new file mode 100644 index 0000000000..617ad81fd9 --- /dev/null +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -0,0 +1,3784 @@ +# Turkish (tr) catalog. Columns, separated by a TAB: context source translation +# (the format is described in ja.tsv). +# +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Turkish computing vocabulary (katman, seçim, maske, ayar katmanı, fırça, degrade ...). No +# proprietary localisation resource, string table or product UI was consulted or copied; add +# entries the same way. Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) +# stay as they are; the trailing "…" of dialog commands is kept. +# + Rows Satırlar + Number Sayı + Top: Üst: + Left: Sol: + Bottom: Alt: + Right: Doğru: + Target: Hedef: + Selected Artboards Seçilen Çalışma Yüzeyleri + Center Columns Merkezi Sütunlar + Clear Existing Guides Mevcut Kılavuzları Temizle + Invalid guide layout Geçersiz kılavuz düzeni + Leave empty for automatic size Otomatik boyut için boş bırakın + 5 Columns, 3 Rows 5 Sütun, 3 Satır + 8 Columns 8 Sütun + 12 Columns 12 Sütun + File Dosya + Edit Düzen + Image Görüntü + Layer Katman + Type Metin + Select Seç + Scope Kapsam + Top Left Sol Üst + Top Right Sağ Üst + Bottom Left Sol Alt + Bottom Right Sağ Alt + Filter Filtre + View Görünüm + Window Pencere + Help Yardım + (coming soon) (yakında) + New… Yeni… + New from Clipboard Pano'dan yeni + Open… Aç… + Open As… Farklı Aç… + Open EXR Parts… EXR Parçalarını açın… + Add beauty precomp from the AOVs AOV'lerden güzellik ön kompozisyonu ekleyin + Both Her ikisi de + Left Sol + Right Sağ + Open Recent Son Kullanılanları Aç + Clear Recent Files Son Dosyaları Temizle + Remove from Recent En Son'dan kaldır + Remove {name} from Recent {name} öğesini En Son'dan kaldır + Close Kapat + Minimize Küçült + Maximize Büyüt + Restore Geri yükle + Close All Tümünü Kapat + Close Others Diğerlerini Kapat + Save Kaydet + Save As… Farklı Kaydet… + Save a Copy… Bir Kopyasını Kaydet… + Revert Geri Döndür + Export Dışa Aktar + Quick Export as PNG PNG Olarak Hızlı Dışa Aktarma + Export As… Farklı Dışa Aktar… + Export Preferences… Dışa Aktarma Tercihleri… + Save for Web (Legacy)… Web İçin Kaydet (Eski)… + Artboards to Files… Çalışma Yüzeylerinden Dosyalara… + Artboards to PDF… Çalışma yüzeylerinden PDF'ye… + Layers to Files… Katmanlardan Dosyalara… + Layer Comps to Files… Dosyalara Katman Kompozisyonları… + Color Lookup Tables… Renk Arama Tabloları… + Data Sets as Files… Dosya Olarak Veri Kümeleri… + Paths to Illustrator… İllüstratöre Giden Yollar… + Render Video… Videoyu Oluştur… + Generate Oluştur + Image Assets Resim Varlıkları + Place Embedded… Gömülü Olarak Yerleştir… + Place Linked… Bağlantılı Olarak Yerleştir… + Package… Paketle… + Automate Otomatikleştir + Batch… Toplu… + Create Droplet… Damlacık oluştur… + Crop and Straighten Photos Fotoğrafları Kırp ve Düzelt + Contact Sheet II… İletişim Sayfası II… + Conditional Mode Change… Koşullu Mod Değişikliği… + Fit Image… Resmi Sığdır… + Lens Correction… Mercek Düzeltme… + Merge to HDR Pro… HDR Pro'ya Birleştir… + Photomerge… Fotoğraf Birleştir… + Scripts Komut dosyaları + Image Processor… Görüntü İşlemcisi… + Delete All Empty Layers Tüm Boş Katmanları Sil + Flatten All Layer Effects Tüm Katman Efektlerini Düzleştir + Flatten All Masks Tüm Maskeleri Düzleştir + Script Events Manager… Komut Dosyası Olay Yöneticisi… + Import İthalat + Browse… Göz at… + Reveal in Finder Finder'da Göster + Show in Explorer Explorer'da Göster + Show in Folder Klasörde Göster + File Info… Dosya Bilgileri… + Print… Yazdır… + Print One Copy Bir Kopya Yazdır + Exit Çıkış + Undo Geri Al + Redo Yinele + Toggle Last State Son Durumu Değiştir + Fade… Solmaya… + Cut Kes + Copy Kopyala + Copy Merged Birleştirilmiş Olarak Kopyala + Paste Yapıştır + Paste Special Özel Yapıştır + Paste in Place Yerine Yapıştır + Paste Into İçine Yapıştır + Paste Outside Dışına Yapıştır + Paste Lorem Ipsum Lorem Ipsum'u Yapıştır + Clear Temizle + Search… Ara… + Check Spelling… Yazımı Denetle… + Find and Replace Text… Metni Bul ve Değiştir… + Fill… Doldur… + Stroke… Kontur… + Content-Aware Fill… İçeriğe Uygun Dolgu… + Delete and Fill Selection Seçimi Sil ve Doldur + Content-Aware Scale İçeriğe Uygun Ölçek + Puppet Warp Kukla Çözgü + Perspective Warp Perspektif Çarpıtma + Free Transform Serbest Dönüştürme + Free Transform a Copy Bir Kopyayı Ücretsiz Dönüştürme + Transform Dönüştür + Again Yinele + Transform Again on a Copy Bir Kopyada Yeniden Dönüştür + Scale Ölçekle + Rotate Döndür + Skew Eğ + Distort Çarpıt + Perspective Perspektif + Warp Deforme Et + Flip Horizontal Yatay Çevir + Flip Vertical Dikey Çevir + Rotate 180° 180° Döndür + Rotate 90° Clockwise 90° Saat Yönünde Döndür + Rotate 90° Counter Clockwise 90° Saat Yönünün Tersine Döndür + 180° 180° + 90° Clockwise 90° Saat Yönünde + 90° Counter Clockwise 90° Saat Yönünün Tersine + Arbitrary… Keyfi… + Auto-Align Layers… Katmanları Otomatik Hizala… + Auto-Blend Layers… Katmanları Otomatik Karıştır… + Define Brush Preset… Fırça Ön Ayarını Tanımla… + Define Pattern… Deseni Tanımla… + Define Custom Shape… Özel Şekil Tanımlayın… + Purge Temizle + Clipboard Pano + Histories Geçmiş + Video Cache Video Önbelleği + All Tümü + Presets Ön ayarlar + Preset Manager… Ön Ayar Yöneticisi… + Migrate Presets Ön Ayarları Taşı + Export/Import Presets… Ön Ayarları Dışa/İçe Aktar… + Color Settings… Renk Ayarları… + Assign Profile… Profil Ata… + Convert to Profile… Profile Dönüştür… + Keyboard Shortcuts… Klavye Kısayolları… + Menus… Menüler… + Toolbar… Araç Çubuğu… + Preferences Tercihler + General… Genel… + Interface… Arayüz… + Workspace… Çalışma alanı… + Tools… Araçlar… + History Log… Geçmiş Günlüğü… + File Handling… Dosya İşleme… + Export… İhracat… + Performance… Performans… + Scratch Disks… Kazı Diskleri… + Cursors… İmleçler… + Transparency & Gamut… Şeffaflık ve Gamut… + Units & Rulers… Birimler ve Cetveller… + Guides, Grid & Slices… Kılavuzlar, Izgaralar ve Dilimler… + Plug-ins… Eklentiler… + Type… Tür… + Enhanced Controls… Gelişmiş Kontroller… + Camera Raw… Kamera Raw… + Raw Defaults… Ham Varsayılanlar… + Integrations… Entegrasyonlar… + AI Integrations… Yapay Zeka Entegrasyonları… + Install Plug-in… Eklentiyi Yükle… + Language Options Dil Seçenekleri + Settings… Ayarlar… + Mode Mod + Bitmap Bit eşlem + Grayscale Gri tonlamalı + Duotone Çift tonlu + Indexed Color İndekslenmiş Renk + RGB Color RGB Rengi + CMYK Color CMYK Rengi + Lab Color Laboratuvar Rengi + Multichannel Çok kanallı + 8 Bits/Channel 8 Bit/Kanal + 8 bit 8 bit + Integer Tamsayı + 16 Bits/Channel 16 Bit/Kanal + 16 bit 16 bit + 32 Bits/Channel 32 Bit/Kanal + 32 bit (float) 32 bit (kayan) + Floating point Kayan nokta + Color Table… Renk Tablosu… + Bitmap… Bit eşlem… + Duotone… Çift tonlu… + Indexed Color… İndekslenmiş Renk… + Adjustments Ayarlamalar + Brightness/Contrast… Parlaklık/Kontrast… + Levels… Düzeyler… + Curves… Eğriler… + Exposure… Pozlama… + Vibrance… Titreşim… + Hue/Saturation… Renk Tonu/Doygunluk… + Color Balance… Renk Dengesi… + Black & White… Siyah ve Beyaz… + Photo Filter… Fotoğraf Filtresi… + Channel Mixer… Kanal Karıştırıcı… + Color Lookup… Renk Arama… + Invert Tersine Çevir + Posterize… Posterleştir… + Threshold… Eşik… + Gradient Map… Degrade Eşlemesi… + Selective Color… Seçici Renk… + Shadows/Highlights… Gölgeler/Vurgular… + HDR Toning… HDR Tonlama… + Desaturate Doygunluğu Azalt + Match Color… Rengi Eşleştir… + Replace Color… Rengi Değiştir… + Equalize Eşitle + Auto Tone Otomatik Ton + Auto Contrast Otomatik Kontrast + Auto Color Otomatik Renk + Image Size… Görüntü Boyutu… + Canvas Size… Tuval Boyutu… + Image Rotation Görüntü Döndürme + Flip Canvas Horizontal Kanvası Yatay Çevir + Flip Canvas Vertical Kanvası Dikey Çevir + Crop Kırp + Trim… Kırp (Kenar)… + Reveal All Tümünü Göster + Duplicate… Çoğalt… + Apply Image… Resmi Uygula… + Calculations… Hesaplamalar… + Variables Değişkenler + Define… Tanımla… + Data Sets… Veri Setleri… + Apply Data Set… Veri Kümesini Uygula… + Variable Data Sets… Değişken Veri Setleri… + Trap… Tuzak… + Analysis Analiz + Set Measurement Scale… Ölçüm Ölçeğini Ayarla… + Select Data Points… Veri Noktalarını Seçin… + Record Measurements Ölçümleri Kaydet + Ruler Tool Cetvel Aracı + Count Tool Sayma Aracı + Place Scale Marker… Ölçek İşaretçisini Yerleştirin… + Custom… Özel… + New Yeni + Layer… Katman… + Group… Grup… + Group from Layers… Katmanlardan Grup… + Artboard… Çalışma yüzeyi… + Artboard from Group… Gruptan çalışma yüzeyi… + Artboard from Layers… Katmanlardan Çalışma Yüzeyi… + Layer via Copy Kopyalama yoluyla katman + Layer via Cut Kesim Yoluyla Katman + Layer From Background… Arka Plandan Katman… + Layer from Background… Arka Plandan Katman… + Background From Layer Katmandan Arka Plan + New Shape Layer Yeni Şekil Katmanı + New Type Layer Yeni Tip Katmanı + New Fill Layer Yeni Dolgu Katmanı + New Adjustment Layer Yeni Ayarlama Katmanı + New Layer Based Slice Yeni Katman Tabanlı Dilim + New Smart Object via Copy Kopyalama Yoluyla Yeni Akıllı Nesne + New Blank Video Layer Yeni Boş Video Katmanı + New Video Layer from File… Dosyadan Yeni Video Katmanı… + Duplicate Layer… Katmanı Çoğalt… + Delete Layer Katmanı Sil + Delete Sil + Rename Layer Katmanı Yeniden Adlandır + Create Layer Katman Oluştur + Layer Style Katman Stili + Blending Options… Karışım Seçenekleri… + Bevel & Emboss… Eğim ve Kabartma… + Use Global Light Global Light'ı kullanın + Inner Shadow… İç Gölge… + Inner Glow… İç Işıma… + Satin… Saten… + Color Overlay… Renk Kaplama… + Gradient Overlay… Degrade Kaplama… + Pattern Overlay… Desen Kaplama… + Outer Glow… Dış Işıma… + Drop Shadow… Alt Gölge… + Copy Layer Style Katman Stilini Kopyala + Paste Layer Style Katman Stilini Yapıştır + Clear Layer Style Katman Stilini Temizle + Global Light… Küresel Işık… + Hide All Effects Tüm Efektleri Gizle + Scale Effects… Ölçek Etkileri… + Smart Filter Akıllı Filtre + Convert for Smart Filters Akıllı Filtreler için Dönüştürme + Clear Smart Filters Akıllı Filtreleri Temizle + Disable Smart Filters Akıllı Filtreleri Devre Dışı Bırak + Delete Filter Mask Filtre Maskesini Sil + Disable Filter Mask Filtre Maskesini Devre Dışı Bırak + Solid Color… Düz Renk… + Gradient… Gradyan… + Pattern… Desen… + Layer Mask Katman Maskesi + Vector Mask vektör maskesi + Reveal Selection Seçimi Göster + Hide Selection Seçimi Gizle + Hide All Tümünü Gizle + From Transparency Şeffaflıktan + Delete Vector Mask Vektör Maskesini Sil + Enable Layer Mask Katman Maskesini Etkinleştir + Disable Layer Mask Katman Maskesini Devre Dışı Bırak + Enable Vector Mask Vektör Maskesini Etkinleştir + Link Layer Mask Bağlantı Katmanı Maskesi + Unlink Layer Mask Katman Maskesinin Bağlantısını Kaldır + Link Vector Mask Bağlantı Vektör Maskesi + Add Vector Mask Vektör Maskesi Ekle + Vector Mask from Current Path Geçerli Yoldan Vektör Maskesi + Create Clipping Mask Kırpma Maskesi Oluştur + Release Clipping Mask Kırpma Maskesini Serbest Bırakın + Smart Objects Akıllı Nesneler + Smart Object Akıllı Nesne + Convert to Smart Object Akıllı Nesneye Dönüştür + Convert to Embedded Gömülüye Dönüştür + Convert to Layers Katmanlara Dönüştür + Convert to Linked… Bağlantılıya Dönüştür… + Edit Contents İçeriği Düzenle + Replace Contents… İçeriği Değiştir… + Relink to File… Dosyaya Yeniden Bağla… + Export Contents… İçeriği Dışa Aktar… + Update Modified Content Değiştirilen İçeriği Güncelle + Update All Modified Content Değiştirilen Tüm İçeriği Güncelle + Stack Mode Yığın Modu + Entropy Entropi + Kurtosis Basıklık + Maximum Maksimum + Mean ortalama + Median Medyan + Minimum Asgari + Range Aralık + Skewness çarpıklık + Standard Deviation Standart Sapma + Summation Toplama + Variance Varyans + Video Layers Video Katmanları + Interpret Footage… Görüntüyü Yorumla… + Replace Footage… Görüntüyü Değiştir… + Reload Frame Çerçeveyi Yeniden Yükle + Show Altered Video Değiştirilen Videoyu Göster + Restore Frame Çerçeveyi Geri Yükle + Restore All Frames Tüm Çerçeveleri Geri Yükle + Insert Blank Frame Boş Çerçeve Ekle + Delete Frame Çerçeveyi Sil + Duplicate Frame Çerçeveyi Çoğalt + Frame from Layers Katmanlardan Çerçeve + Video Frames to Layers… Video Karelerinden Katmanlara… + Rasterize Rasterleştir + Rasterize Type Rasterleştirme Türü + Rasterize Type Layer Tip Katmanını Rasterleştir + Rasterize Shape Şekli Rasterleştir + New Guide Layout… Yeni Kılavuz Düzeni… + New Guides From Shape Shape'den Yeni Kılavuzlar + Lock Layers… Katmanları Kilitle… + Link Layers Katmanları Bağla + Select Linked Layers Bağlantılı Katmanları Seçin + Merge Layers Katmanları Birleştir + Merge Down Aşağı Birleştir + Merge Visible Görünürleri Birleştir + Stamp Visible Damga Görünür + Select Layer Above Yukarıdaki Katmanı Seçin + Select Layer Below Aşağıdaki Katmanı Seçin + Select Top Layer Üst Katmanı Seçin + Select Bottom Layer Alt Katmanı Seçin + Add Layer Above to Selection Seçime Yukarıdaki Katmanı Ekle + Add Layer Below to Selection Aşağıdaki Katmanı Seçime Ekle + Stamp Down Damgalama + Flatten Image Görüntüyü Düzleştir + Matting Paspas + Color Decontaminate… Renkli Dekontamine… + Remove Black Matte Siyah Matı Kaldır + Remove White Matte Beyaz Matı Kaldır + Defringe… Borcu kaldır… + Group Layers Grup Katmanları + Ungroup Layers Katmanların Grubunu Çöz + Hide Layers Katmanları Gizle + Arrange Sırala + Bring to Front En Öne Getir + Bring Forward Öne Getir + Send Backward Arkaya Gönder + Send to Back En Arkaya Gönder + Reverse Ters + Combine Shapes Şekilleri Birleştir + Unite Shapes Şekilleri Birleştir + Subtract Front Shape Ön Şekli Çıkart + Intersect Shape Areas Şekil Alanlarını Kesiş + Exclude Overlapping Shapes Çakışan Şekilleri Hariç Tut + Merge Shape Components Şekil Bileşenlerini Birleştir + Align Hizala + Top Edges Üst Kenarlar + Vertical Centers Dikey Merkezler + Bottom Edges Alt Kenarlar + Left Edges Sol Kenarlar + Horizontal Centers Yatay Merkezler + Right Edges Sağ Kenarlar + Distribute Dağıt + Vertically Dikey olarak + Horizontally Yatay olarak + Layer Edges Katman Kenarları + Isolate Layers Katmanları Yalıt + Layer Content Options… Katman İçerik Seçenekleri… + Fill Content İçeriği Doldur + Deselect Layers Katmanların Seçimini Kaldır + Find Layers Katmanları Bul + Layer Comps Katman Kompozisyonları + Layers Katmanlar + All Layers Tüm Katmanlar + Hidden Layers Gizli Katmanlar + Mask All Objects Tüm Nesneleri Maskele + Subject Konu + Sky Gökyüzü + Panels Paneller + Character Panel Karakter Paneli + Paragraph Panel Paragraf Paneli + Character Styles Panel Karakter Stilleri Paneli + Paragraph Styles Panel Paragraf Stilleri Paneli + Glyphs Panel Glifler Paneli + Anti-Alias Kenar Yumuşatma + Sharp Keskin + Crisp Çıtır + Strong Güçlü + Smooth Pürüzsüz + Windows LCD Windows LCD'si + Windows Windows + Orientation Oryantasyon + Horizontal Yatay + Vertical Dikey + OpenType OpenType + Standard Ligatures Standart Bitişik Harfler + Contextual Alternates Bağlamsal Alternatifler + Discretionary Ligatures İsteğe Bağlı Bitişik Harfler + Swash çalkalama + Oldstyle Eski tarz + Stylistic Alternates Stilistik Alternatifler + Titling Alternates Başlık Alternatifleri + Ornaments Süsler + Ordinals Sıra Sayıları + Fractions Kesirler + Create Work Path İş Yolu Oluştur + Convert to Shape Şekle Dönüştür + Warp Text… Metni Çözgü… + Convert to Point Text Nokta Metnine Dönüştür + Convert to Paragraph Text Paragraf Metnine Dönüştür + Default Features Varsayılan Özellikler + East Asian Features Doğu Asya Özellikleri + Middle Eastern Features Orta Doğu Özellikleri + Middle Eastern & South Asian Composer Orta Doğulu ve Güney Asyalı Besteci + Update All Text Layers Tüm Metin Katmanlarını Güncelle + Replace All Missing Fonts Tüm Eksik Yazı Tiplerini Değiştir + Resolve Missing Fonts… Eksik Yazı Tiplerini Çözün… + Font Preview Size Yazı Tipi Önizleme Boyutu + None Yok + Small Küçük + Medium Orta + Large Büyük + Extra Large Ekstra Büyük + Huge Devasa + Load Default Type Styles Varsayılan Yazım Stillerini Yükle + Save Default Type Styles Varsayılan Yazım Stillerini Kaydet + Deselect Seçimi Kaldır + Reselect Yeniden Seç + Inverse Ters Çevir + Inverse Selection Ters Seçim + Similar Benzer + Color Range… Renk Aralığı… + Focus Area… Odaklanma Alanı… + Select and Mask… Seç ve Maskele… + Smart Radius Akıllı Yarıçap + Shift Edge Kenarı Kaydır + Decontaminate Dekontaminasyon + Refine Edge Kenarı İyileştir + Modify Değiştir + Border… Kenarlık… + Smooth… Düzelt… + Expand… Genişlet… + Contract… Daralt… + Feather… Geçiş Yumuşatma… + Grow Büyüt + Transform Selection Seçimi Dönüştür + Edit in Quick Mask Mode Hızlı Maske Modunda Düzenleme + Load Selection… Seçimi Yükle… + Save Selection… Seçimi Kaydet… + Match All Tümünü Eşleştir + Last Filter Son Filtre + Filter Gallery… Galeriyi Filtrele… + Adaptive Wide Angle… Uyarlanabilir Geniş Açı… + Camera Raw Filter… Camera Raw Filtresi… + Liquify… Sıvılaştır… + Vanishing Point… Ufuk Noktası… + Blur Bulanıklaştırma + Blur Gallery Bulanıklaştırma Galerisi + Field Blur… Alan Bulanıklığı… + Iris Blur… İris Bulanıklığı… + Tilt-Shift… Tilt-Shift… + Path Blur… Yol Bulanıklığı… + Spin Blur… Döndürme Bulanıklığı… + Blur More Daha Fazla Bulanıklaştır + Box Blur… Kutu Bulanıklığı… + Gaussian Blur… Gauss Bulanıklığı… + Lens Blur… Mercek Bulanıklığı… + Motion Blur… Hareket Bulanıklığı… + Radial Blur… Radyal Bulanıklık… + Shape Blur… Şekil Bulanıklığı… + Smart Blur… Akıllı Bulanıklaştırma… + Surface Blur… Yüzey Bulanıklığı… + Average Ortalama + Displace… Yer değiştir… + Pinch… Tut… + Polar Coordinates… Kutupsal Koordinatlar… + Ripple… Dalgalanma… + Shear… Kesme… + Spherize… Küreselleştir… + Twirl… Döndür… + Wave… Dalga… + ZigZag… ZigZag… + Noise Gürültü + Add Noise… Gürültü Ekle… + Despeckle Benek giderme + Dust & Scratches… Toz ve Çizikler… + Median… Medyan… + Reduce Noise… Gürültüyü Azaltın… + Pixelate Pikselleştir + Color Halftone… Renk Yarım Tonu… + Crystallize… Kristalleşin… + Facet Faset + Fragment Parça + Mezzotint… Mezzotint… + Mosaic… Mozaik… + Pointillize… Noktalaştırın… + Render İşleme + Clouds Bulutlar + Difference Clouds Fark Bulutları + Fibers… Lifler… + Flame… Alev… + Lens Flare… Mercek Parlaması… + Lighting Effects… Aydınlatma Efektleri… + Relight… Yeniden aydınlat… + Picture Frame… Resim Çerçevesi… + Tree… Ağaç… + Sharpen Keskinleştir + Sharpen Edges Kenarları Keskinleştirin + Sharpen More Daha Fazla Keskinleştir + Smart Sharpen… Akıllı Keskinleştirme… + Unsharp Mask… Keskinliği Azaltma Maskesi… + High Pass… Yüksek Geçiş… + Stylize Stilize Et + Diffuse… Yaygın… + Emboss… Kabartma… + Extrude… Ekstrüzyon… + Find Edges Kenarları Bul + Oil Paint… Yağlı Boya… + Solarize Solarizasyon + Tiles… Fayans… + Trace Contour… Konturu İzle… + Wind… Rüzgar… + Video video + De-Interlace… Taramasızlaştırma… + NTSC Colors NTSC Renkleri + Other Diğer + Maximum… Maksimum… + Minimum… Asgari… + Offset… Ofset… + Plug-ins Eklentiler + Proof Setup Prova Kurulumu + Proof Colors Prova Renkleri + Gamut Warning Gamut Uyarısı + Working CMYK CMYK'de çalışıyor + Working Cyan Plate Çalışan Camgöbeği Plaka + Working Magenta Plate Çalışan Macenta Plaka + Working Yellow Plate Sarı Plaka Çalışması + Working Black Plate Siyah Plaka Çalışma + Working CMY Plate Çalışan CMY Plakası + Legacy Macintosh RGB Eski Macintosh RGB + Internet Standard RGB İnternet Standardı RGB + Monitor RGB RGB'yi izleyin + Color Blindness — Protanopia-type Renk Körlüğü — Protanopi tipi + Color Blindness — Deuteranopia-type Renk Körlüğü – Döteranopi tipi + Zoom In Yakınlaştır + Zoom Out Uzaklaştır + Fit on Screen Ekrana Sığdır + Fit Layer(s) on Screen Katmanları Ekrana Sığdır + Fit Artboard on Screen Çalışma Yüzeyini Ekrana Sığdır + 100% 100% + 200% 200% + Print Size Baskı Boyutu + Pixel Aspect Ratio Piksel En Boy Oranı + Pixel Aspect Ratio Correction Piksel En Boy Oranı Düzeltme + Custom Pixel Aspect Ratio… Özel Piksel En Boy Oranı… + Square Kare + D1/DV NTSC (0.91) D1/DV NTSC (0,91) + D1/DV NTSC Widescreen (1.21) D1/DV NTSC Geniş Ekran (1.21) + D1/DV PAL (1.09) D1/DV PAL (1,09) + D1/DV PAL Widescreen (1.46) D1/DV PAL Geniş Ekran (1,46) + Anamorphic 2:1 (2) Anamorfik 2:1 (2) + HDV 1080/DVCPRO HD 720 (1.33) HDV 1080/DVCPRO HD 720 (1,33) + DVCPRO HD 1080/HDV 1080 (1.5) DVCPRO HD 1080/HDV 1080 (1,5) + 32-bit Preview Options… 32-bit Önizleme Seçenekleri… + Screen Mode Ekran Modu + Standard Screen Mode Standart Ekran Modu + Full Screen Mode With Menu Bar Menü Çubuklu Tam Ekran Modu + Full Screen Mode Tam Ekran Modu + Cycle Screen Mode Döngü Ekranı Modu + Show Göster + Extras Ekstralar + Selection Edges Seçim Kenarları + Target Path Hedef Yolu + Current Path Geçerli Yol + Grid Izgara + Guides Kılavuzlar + Canvas Guides Kanvas Kılavuzları + Artboard Guides Çalışma Yüzeyi Kılavuzları + Count Sayım + Smart Guides Akıllı Kılavuzlar + Slices Dilimler + Notes Notlar + Pixel Grid Piksel Izgarası + Pixel Art Piksel Sanatı + Pixel Art Preview Piksel Sanat Önizlemesi + Pattern Preview Desen Önizlemesi + Brush Preview Fırça Önizlemesi + Mesh ağ + Edit Pins Pinleri Düzenle + Document Bounds Belge Sınırları + Show Extras Options… Ekstra Seçeneklerini Göster… + Rulers Cetveller + Snap Yapış + Snap To Şuna Yapış + Lock Guides Kılavuzları Kilitle + Clear Guides Kılavuzları Temizle + Clear Selected Artboard Guides Seçilen Çalışma Yüzeyi Kılavuzlarını Temizle + Clear Canvas Guides Şeffaf Kanvas Kılavuzları + New Guide… Yeni Kılavuz… + Lock Slices Dilimleri Kilitle + Clear Slices Dilimleri Temizle + Match Zoom Yakınlaştırmayı Eşleştir + Match Location Eşleşme Konumu + Match Rotation Maç Rotasyonu + Cascade Çağlayan + Tile Fayans + Tile All Horizontally Tümünü Yatay Olarak Döşe + Tile All Vertically Tümünü Dikey Olarak Döşe + 2-up Horizontal 2'li Yatay + 2-up Vertical 2'li Dikey + 3-up Horizontal 3'lü Yatay + 3-up Stacked 3'lü Yığılmış + 3-up Vertical 3'lü Dikey + 4-up 4'lü + 6-up 6'lı + Consolidate All to Tabs Tümünü Sekmelerde Birleştir + Float in Window Pencerede Kaydır + Float All in Windows Tümünü Windows'ta Kaydır + New Window for Document Belge için Yeni Pencere + Workspace Çalışma alanı + Essentials Temel bilgiler + Graphic and Web Grafik ve Web + Painting Boyama + Photography Fotoğrafçılık + New Workspace… Yeni Çalışma Alanı… + Delete Workspace… Çalışma Alanını Sil… + Reset Workspace Çalışma Alanını Sıfırla + Reset Tool Aracı Sıfırla + Reset All Tools Tüm Araçları Sıfırla + Reset all tools to their default settings? Tüm araçlar varsayılan ayarlarına sıfırlansın mı? + Lock Workspace Çalışma Alanını Kilitle + Theme Tema + Pro Theme Profesyonel Tema + Pro Medium Gray Theme Pro Orta Gri Tema + Pro Light Gray Theme Pro Açık Gri Tema + Pro Light Theme Profesyonel Işık Teması + Studio Theme Stüdyo Teması + Studio Light Theme Stüdyo Işık Teması + Classic Theme Klasik Tema + Solarized dark Solarize karanlık + Solarized Dark Theme Solarize Karanlık Tema + Breeze dark Esinti karanlık + Breeze Dark Theme Esinti Koyu Tema + Breeze light Esinti ışığı + Breeze Light Theme Esinti Işık Teması + Adwaita Light Theme Adwaita Işık Teması + Adwaita Dark Theme Adwaita Karanlık Tema + Adwaita Adwaita + Adwaita dark Adwaita karanlık + Next Theme Sonraki Tema + Actions Eylemler + Brush Settings Fırça Ayarları + Brushes Fırçalar + Channels Kanallar + Character Karakter + Character Styles Karakter Stilleri + Clone Source Klon Kaynağı + Color Renk + Glyphs Glifler + Gradients Degradeler + Histogram Histogram + History Tarih + Info Bilgi + Measurement Log Ölçüm Günlüğü + Modifier Keys Değiştirici Tuşlar + Navigator Gezgin + Options Seçenekler + Paragraph Paragraf + Paragraph Styles Paragraf Stilleri + Paths Yollar + Patterns Desenler + Properties Özellikler + Shapes Şekiller + Styles Stiller + Swatches Renk Örnekleri + Timeline Zaman çizelgesi + Tool Presets Araç Ön Ayarları + Tools Araçlar + About PhotoCraft PhotoCraft Hakkında + PhotoCraft Website PhotoCraft Web Sitesi + PhotoCraft on GitHub GitHub'da PhotoCraft + ArtCraft Website ArtCraft Web Sitesi + Join the ArtCraft Discord… ArtCraft Discord'una katılın… + Report an Issue… Bir Sorun Bildirin… + WIA Support… WIA Desteği… + Apply Uygula + HSB/HSL HSB/HSL + Invert… Ters çevir… + Load Files into Stack… Dosyaları Yığına Yükle… + Motion Hareket + Notes… Notlar… + Other… Diğer… + Remove Warp Split Warp Split'i Kaldır + Shape Şekil + Split Warp Crosswise Çözgüyü Çapraz Olarak Böl + Split Warp Horizontally Çözgüyü Yatay Olarak Böl + Split Warp Vertically Çözgüyü Dikey Olarak Böl + Statistics… İstatistikler… + Refine Strokes Konturları İyileştir +@plural {n} item|{n} items {n} öğe + Auto Otomatik + (print to the printer) (yazıcıya yazdırın) + + Add variable + Değişken ekle + 30 frames @ 30 fps 30 kare @ 30 fps + 3D LUT File 3D LUT Dosyası + Absolute Mutlak + Absolute Colorimetric Mutlak Kolorimetrik + Action Eylem + Adaptive Uyarlanabilir + Adaptive Wide Angle isn't open Uyarlanabilir Geniş Açı açık değil + Add Ekle + Add Layer Mask Katman Maskesi Ekle + Add Stop Durak Ekle + Add a layer style Katman stili ekleme + Add a mask (from the selection; {key} inverts) Bir maske ekleyin (seçimden; {key} tersine çevirir) + Add an adjustment Düzenleme ekle + Add disk Disk ekle + Add space after paragraph Paragraftan sonra boşluk ekle + Add space before paragraph Paragraftan önce boşluk ekleyin + Add to Sample Örneğe Ekle + Add to selection ({key}) Seçime ekle ({key}) + Adjust for Zoom Yakınlaştırma için Ayarlama + Adjustment visible Ayar görünür + Adobe RGB (1998) compatible Adobe RGB (1998) uyumlu + Align and Distribute Hizala ve Dağıt + Align and distribute Hizala ve dağıt + Align horizontal centers Yatay merkezleri hizalayın + Align left edges Sol kenarları hizala + Align right edges Sağ kenarları hizalayın + Aligned Hizalı + All Caps Tamamı Büyük Harf + All Slices Tüm Dilimler + All User Slices Tüm Kullanıcı Dilimleri + All shortcuts reset to PhotoCraft defaults. Tüm kısayollar PhotoCraft varsayılanlarına sıfırlanır. + Alt Alternatif + Always use pressure for opacity Opaklık için her zaman baskıyı kullanın + Always use pressure for size Her zaman boyuta göre baskı kullanın + Amount Tutar + Anchor: Çapa: + Angle Açı + Angle Gradient Açı Gradyanı + Angle Jitter Açı Değişimi + Answer the unsaved-changes prompt first Önce kaydedilmemiş değişiklikler istemini yanıtlayın + Anti-alias Kenar yumuşatma + Appearance Görünüm + Appearance (Layer Style) Görünüm (Katman Stili) + Apply Layer Mask Katman Maskesini Uygula + Apply Per Tip İpucu Başına Uygula + Apply next selected layer comp Sonraki seçilen katman kompozisyonunu uygula + Apply previous selected layer comp Önceki seçilen katman kompozisyonunu uygula + Artboard Çalışma yüzeyi + Artboard Background Color Çalışma Yüzeyi Arka Plan Rengi + Assign working space Çalışma alanı atayın + Author: Yazar: + Auto Hide Otomatik Gizle + Auto-Select: Otomatik Seç: + Automatically level near-horizontal line segments Yataya yakın çizgi bölümlerini otomatik olarak hizalayın + Automatically straighten and level Otomatik olarak düzeltin ve seviyelendirin + Automatically straighten near-vertical line segments Dikeye yakın çizgi bölümlerini otomatik olarak düzelt + Background Arka plan + Background Color Arka Plan Rengi + Background Contents Arka Plan İçeriği + Balance Bakiye + Baseline Temel + Baseline shift Taban çizgisi kayması + Basic Temel + Before (Y) Önce (Y) + Begin recording Kaydı başlat + Bend: Bükülme: + Bicubic Bikübik + Bicubic (smooth gradients) Bikübik (düzgün degradeler) + Bicubic Smoother (enlargement) Bikübik Pürüzsüzleştirici (büyütme) + Bicubic Sharper (reduction) Bikübik Daha Keskin (azaltma) + Bilinear Çift doğrusal + Black Siyah + Black & White Siyah Beyaz + Black Point Compensation Kara Nokta Telafisi + Black, White Siyah, Beyaz + Blacks Siyahlar + Blending Karıştırma + Blending Options Karıştırma Seçenekleri + Blue Mavi + Blue, Red, Yellow Mavi, Kırmızı, Sarı + Both Axes Her İki Eksen + Bottom Alt + Brightness Parlaklık + Brightness Jitter Parlaklık Değişimi + Brightness/Contrast Parlaklık/Kontrast + Bring the selected pin forward Seçilen pini öne getir + Brush Preset picker Fırça Ön Ayarı seçici + Brush Reconstruct Options Fırçayla Yeniden Yapılandırma Seçenekleri + Brush Tool Options Fırça Aracı Seçenekleri + Cache Level: Önbellek Düzeyi: + Camera Raw isn't open Camera Raw açık değil + Cancel İptal + Reset Sıfırla + Cancel (Esc) İptal (Esc) + Cancel any current edits (Esc) Mevcut düzenlemeleri iptal et (Esc) + Cancel current crop operation (Esc) Geçerli kırpma işlemini iptal et (Esc) + Cancel transform (Esc) Dönüşümü iptal et (Esc) + Cancel warp (Esc) Çözgüyü iptal et (Esc) + Canvas Kanvas + Canvas extension color: Kanvas uzatma rengi: + Capture Yakala + Capture the visibility, position and style of every layer with + below, then switch between versions. Aşağıdaki + ile her katmanın görünürlüğünü, konumunu ve stilini yakalayın, ardından sürümler arasında geçiş yapın. + Catch-up on Stroke End Strok Sonunda Yakalama + Center Merkez + Center Crop Marks Orta Kırpma İşaretleri + Center text Metni ortala + Change Değiştir + Change All Tümünü Değiştir + Change Screen Mode (F) Ekran Modunu Değiştir (F) + Change To: Şuna Değiştir: + Channel Mixer Kanal Mikseri + Channel: Kanal: + Check All Layers Tüm Katmanları Kontrol Et + Check Spelling Yazımı Kontrol Et + Clarity Netlik + Clear All Tümünü Temizle + Clear Override Geçersiz Kılmayı Temizle + Clear Samples Örnekleri Temizle + Click sets foreground · right-click sets background Ön planı ayarlar'a tıklayın · arka planı sağ tıklayın + Click the first point or press {key} to close · Esc cancels Kapatmak için ilk noktayı tıklayın veya {key} tuşuna basın · Esc iptal eder + Click to add a point · drag off or {key}-click or Delete to remove Nokta eklemek için tıklayın · sürükleyin veya {key} tuşuna basıp tıklayın veya kaldırmak için Sil + Click to apply, {key}-click to clear overrides, double-click for Style Options Uygulamak için tıklayın, geçersiz kılmaları temizlemek için {key}-tıklayın, Stil Seçenekleri için çift tıklayın + Click to edit the gradient Degradeyi düzenlemek için tıklayın + Click to restore the current colour Mevcut rengi geri yüklemek için tıklayın + Click to zoom in · {key}-click to zoom out · drag right/left to zoom in/out Yakınlaştırmak için tıklayın · {key}-uzaklaştırmak için tıklayın · yakınlaştırmak/uzaklaştırmak için sağa/sola sürükleyin + Click with the Note tool to add a note. Not eklemek için Not aracıyla tıklayın. + Click: corner · Drag: smooth · Click first point: close · {key} finish · Esc cancel Tıklayın: köşe · Sürükleyin: pürüzsüz · İlk noktaya tıklayın: kapat · {key} bitiş · Esc iptal + Click: corner · Drag: smooth · {alt}-drag: break handle · Click or drag first point: close · {key} finish · Esc cancel Tıklayın: köşe · Sürükleyin: pürüzsüz · {alt}-sürükleyin: tutamacı kırın · İlk noktayı tıklayın veya sürükleyin: kapat · {key} bitiş · Esc iptal + Clip to layer below Aşağıdaki katmana klip + Clipped Kırpılmış + Close Style Options Stil Seçeneklerini Kapat + Cmd Komut + Coated CMYK Kaplamalı CMYK + Color & Swatches Renk ve Renk Örnekleri + Color Balance Renk Dengesi + Color Detail Renk Detayı + Color Grading Renk Derecelendirmesi + Color Lookup Renk Arama + Color Management Renk Yönetimi + Color Mixer Renk Karıştırıcı + Color Mode Renk Modu + Color Priority Renk Önceliği + Color Range preview Renk Aralığı önizlemesi + Color: Renk: + Colorize Renklendir + Colors Renkler + Commit Perspective Warp ({key}) Perspektif Çarpıtma İşlemi ({key}) + Commit Puppet Warp ({key}) Kukla Çarpıtma İşlemi ({key}) + Commit any current edits ({key}) Mevcut düzenlemeleri tamamlayın ({key}) + Commit current crop operation ({key}) Geçerli kırpma işlemini gerçekleştir ({key}) + Commit transform ({key}) Dönüşümü kaydet ({key}) + Commit warp ({key}) Çözgü gerçekleştir ({key}) + Constant Sabit + Constrain proportions Oranları sınırla + Contact Sheet II İletişim Sayfası II + Content-Aware İçeriğe Duyarlı + Contiguous Bitişik + Contrast Kontrast + Control: Kontrol: + Convert document's colors to the working space Belgenin renklerini çalışma alanına dönüştürün + Convert to sRGB sRGB'ye dönüştür + Copies Kopyalar + Copper Bakır + Copyright Telif hakkı + Copyright and Contact Info Telif Hakkı ve İletişim Bilgileri + Corner Crop Marks Köşe Kırpma İşaretleri + Corner radius Köşe yarıçapı + Correction Düzeltme + Count Jitter Titreşimi say + Create Oluştur + Create Droplet Damlacık Oluştur + Create Texture Doku Oluştur + Create Video Timeline Video Zaman Çizelgesi Oluşturun + Create a new count group Yeni bir sayım grubu oluşturun + Create a new document or open an existing file. Yeni bir belge oluşturun veya mevcut bir dosyayı açın. + Create a new group Yeni bir grup oluştur + Create a new layer Yeni bir katman oluştur + Create new action Yeni eylem oluştur + Create new channel Yeni kanal oluştur + Create new document from current state Mevcut durumdan yeni belge oluştur + Create new fill or adjustment layer Yeni dolgu veya ayarlama katmanı oluştur + Create new gradient Yeni degrade oluştur + Create new group Yeni grup oluştur + Create new layer comp Yeni katman kompozisyonu oluştur + Create new pattern from the selection Seçimden yeni desen oluştur + Create new shape from the current path Geçerli yoldan yeni şekil oluştur + Create new snapshot Yeni anlık görüntü oluştur + Create new style (from the selected type) Yeni stil oluştur (seçilen türden) + Create new style from the selected layer Seçilen katmandan yeni stil oluştur + Create new tool preset Yeni araç ön ayarı oluştur + Crop Factor Mahsul Faktörü + Ctrl Ctrl + Current & Below Güncel ve Aşağıda + Current Layer Geçerli Katman + Current Size Mevcut Boyut + Current Tool Only Yalnızca Geçerli Araç + Curve Eğri + Curves Eğriler + Custom Özel + Custom Shapes Özel Şekiller + Cyan Camgöbeği + Cyan · Red Camgöbeği · Kırmızı + Darken Koyulaştır + Darks Karanlıklar + Data Sets Veri Setleri + Default Varsayılan + Default colours (D) Varsayılan renkler (D) + Default printer Varsayılan yazıcı + Default restored. Varsayılan geri yüklendi. + Define Tanımla + Define at least one variable first. İlk önce en az bir değişken tanımlayın. + Dehaze Buğu giderme + Delete Cropped Pixels Kırpılmış Pikselleri Sil + Delete Group Grubu Sil + Delete Layer Mask Katman Maskesini Sil + Delete Layers Katmanları Sil + Delete Shortcut Kısayolu Sil + Delete Timeline Zaman Çizelgesini Sil + Delete Tool Preset Araç Ön Ayarını Sil + Delete count group Sayım grubunu sil + Delete current channel Mevcut kanalı sil + Delete current state Mevcut durumu sil + Delete effect layer Efekt katmanını sil + Delete layer ‘{name}’? '{name}' katmanı silinsin mi? + Delete {count} layers? {names} {count} katman silinsin mi? {names} + Delete layer? Katman silinsin mi? + The document is closed Belge kapatıldı + The document or selected layers changed. Please request deletion again. Belge veya seçilen katmanlar değişti. Lütfen tekrar silme talebinde bulunun. + Missing delete command Silme komutu eksik + Invalid delete command Geçersiz silme komutu + Layer deleted, but confirmation preference could not be saved: {error} Katman silindi ancak onay tercihi kaydedilemedi: {error} + Delete layer Katmanı sil + Delete layer comp Katman kompozisyonunu sil + Delete note Notu sil + Delete selected measurements Seçilen ölçümleri sil + Delete style Stili sil + Delete tool preset Araç ön ayarını sil + Density Yoğunluk + Density: Yoğunluk: + Depth Derinlik + Depth Jitter Derinlik Değişimi + Description Açıklama + Deselect All Tümünün Seçimini Kaldır + Detail Detay + Diamond Gradient Elmas Degrade + Difference Fark + Diffusion Difüzyon + Discard the embedded profile (don't color manage) Gömülü profili atın (renk yönetimi yapmayın) + Discontiguous bitişik değil + Discord Discord + Dismiss Reddet + Display P3 Display P3 + Display Text Metni Görüntüle + Dither Titreşim + Divide… Böl… + Document Belge + Don't Save Kaydetme + No Hayır + Yes Evet + Done Bitti + Dots Noktalar + Drag a rectangle around the object Nesnenin çevresine bir dikdörtgen sürükleyin + Drag on the image to add a constraint; Shift for horizontal/vertical; right-click to delete. Kısıtlama eklemek için resmin üzerine sürükleyin; Yatay/dikey için kaydırma; silmek için sağ tıklayın. + Drag to move the active shape's path or the Work Path Etkin şeklin yolunu veya Çalışma Yolunu taşımak için sürükleyin + Click: select anchor · Drag: move anchor, handle or segment · {shift}-click: add · {alt}-click: whole subpath Tıklayın: çapayı seçin · Sürükleyin: çapayı, tutamacı veya parçayı taşıyın · {shift}-tıklayın: ekleyin · {alt}-tıklayın: alt yolun tamamı + Draw with the Pen tool (P) or make a work path from a selection. Kalem aracıyla (P) çizim yapın veya seçimden bir çalışma yolu oluşturun. + Drop an image or PSD anywhere to open it. Açmak için bir görüntüyü veya PSD'yi herhangi bir yere bırakın. + Native file drag-and-drop is unavailable Yerel dosya sürükle ve bırak özelliği kullanılamıyor + Native file drag-and-drop is not supported on Wayland yet. Use File › Open, or copy the image in your file manager and paste it with {paste}. Yerel dosya sürükle ve bırak özelliği henüz Wayland'de desteklenmiyor. Dosyayı Kullan › Aç veya resmi dosya yöneticinize kopyalayıp {paste} ile yapıştırın. + To drop files, start PhotoCraft under XWayland: `{command}` Dosyaları bırakmak için XWayland altında PhotoCraft'ı başlatın: `{command}` + Or set Preferences › Performance › Linux display server to X11: PhotoCraft then always starts under XWayland. Veya Tercihler › Performans › Linux görüntüleme sunucusunu X11: PhotoCraft olarak ayarlayın, ardından her zaman XWayland altında başlar. + Edit › Paste Düzenle › Yapıştır + Duplicate Layers… Katmanları Çoğalt… + Duplicate style Yinelenen stil + Edit Toolbar… Araç Çubuğunu Düzenle… + Use File › Open, or paste a copied image with {paste}. Dosya › Aç'ı kullanın veya kopyalanan görüntüyü {paste} ile yapıştırın. + Edit in Quick Mask Mode (Q) Hızlı Maske Modunda Düzenleme (Q) + Edit in Standard Mode (Q) Standart Modda Düzenleme (Q) + Effects Efektler + Embed Color Profile Renk Profilini Göm + Embedded Profile Mismatch Gömülü Profil Uyuşmazlığı + Enable airbrush-style build-up effects Airbrush stili oluşturma efektlerini etkinleştirin + Enhance Edge Kenarı Geliştir + Entire Font Yazı Tipinin Tamamı + Exact tam + Expansion: Genişleme: + Export Presets Ön Ayarları Dışa Aktarma + Export selected measurements (CSV) Seçilen ölçümleri dışa aktar (CSV) + Exposure Pozlama + Exposure and Gamma Pozlama ve Gama + Exposure: Pozlama: + Eyedropper Damlalık + Factor: Faktör: + Faux Bold Sahte Kalın + Faux Italic Sahte İtalik + Feather Tüy + Fewer Points Daha Az Puan + File Settings Dosya Ayarları + Fill Doldur + Fill: Doldurun: + Filter for adjustment layers Ayarlama katmanları için filtre + Filter for pixel layers Piksel katmanları için filtre + Filter for shape layers Şekil katmanları için filtre + Filter for smart objects Akıllı nesneler için filtre + Filter for type layers Yazım katmanları için filtre + Filter: Filtre: + Finger Painting Parmak Boyama + Fisheye Balıkgözü + Fit Uygun + Fit Screen Ekrana Sığdır + Fill Screen Doldurma Ekranı + Fit in View Görünüme Sığdır + Fixed Sabit + Fixed Ratio Sabit Oran + Fixed Size Sabit Boyut + Flip Çevir + Flip X X'i Çevir + Flip X Jitter X Değişimini Çevir + Flip Y Y Çevirme + Flip Y Jitter Çevir Y Titreşimi + Flow Akış + Flow Jitter Akış Değişimi + Focal Length Odak Uzunluğu + Font Yazı tipi + Font Size: Yazı Tipi Boyutu: + Font size Yazı tipi boyutu + Foreground Ön plan + Foreground to Background Ön Plandan Arka Plana + Foreground/Background Jitter Ön Plan/Arka Plan Değişimi + Format Biçim + Frame from Layers… Katmanlardan Çerçeve… + Free Ücretsiz + Full Spherical Tam Küresel + Fuzziness: Bulanıklık: + GPU GPU + Gamma Correction Gama Düzeltmesi + Gamma: Gama: + Global Küresel + Glow Color Parıltılı Renk + Gradient Map Gradyan Haritası + Grain Tahıl + Gray Gri + Gray Gamma 2.2 Gri Gama 2.2 + Green Yeşil + Group Grup + Hardness Sertlik + Heart Kalp + Height Yükseklik + Height: Yükseklik: + Hide Auto Slices Otomatik Dilimleri Gizle + Hide Properties Özellikleri Gizle + Highlight Compression Sıkıştırmayı Vurgula + Highlight Priority Önceliği Vurgula + Highlights Öne Çıkanlar + Home Ana sayfa + Horizontal scale Yatay ölçek + Hue Renk Tonu + Hue Jitter Ton Değişimi + Hue/Saturation Ton/Doygunluk + Hyphenate Tireleme + Ignore Yoksay + Ignore All Tümünü Yoksay + Image Size Resim Boyutu + Import Brushes… Fırçaları İçe Aktar… + Import brushes (.abr) Fırçaları içe aktar (.abr) + Import Presets Ön Ayarları İçe Aktar + Imported İthal + Indent First Önce Girintiyi Girin + Indent Left Sola Girinti + Indent Right Sağa Girinti + Indent first line İlk satıra girinti koy + Indent left margin Sol kenar boşluğunu girintile + Indent right margin Sağ kenar boşluğuna girinti + Input: Giriş: + Inside İçeride + Interlaced Taramalı + Interpolation: Enterpolasyon: + Intersect with selection ({key}) Seçimle kesiştir ({key}) + Invert All Tümünü Ters Çevir + Invert has no settings. Tersine çevirmenin hiçbir ayarı yoktur. + Italic italik + Join us on Discord Discord'da bize katılın + Justify all Hepsini gerekçelendir + Justify last centered Son ortayı iki yana yasla + Justify last left Son sola yasla + Justify last right Son sağa yasla + Kerning Karakter Aralığı + Keyboard Shortcuts Klavye Kısayolları + Keyboard Shortcuts and Menus Klavye Kısayolları ve Menüler + Kind tür + Label Size Etiket Boyutu + Labels Etiketler + Lanczos (sharp) Lanczos (keskin) + Landscape Manzara + Last Document State Son Belge Durumu + Layout Düzen + Leading Lider + Leading (set to the font size × auto-leading when Auto) Satır Aralığı (Otomatik olduğunda yazı tipi boyutu × otomatik satır aralığına ayarlanır) + Leave as is (don't color manage) Olduğu gibi bırakın (renk yönetimi yapmayın) + Left align text Metni sola hizala + Length: Uzunluk: + Level Seviye + Levels Seviyeler + Lighten Açıklaştır + Lightness hafiflik + Lights Işıklar + Limits: Sınırlar: + Linear Gradient Doğrusal Degrade + Link layers Katmanları bağlayın + Link width and height Bağlantı genişliği ve yüksekliği + Liquify is not open Sıvılaştırma açık değil + Load Yükle + Load 3D LUT… 3D LUT'u yükleyin… + Load channel as selection Kanalı seçim olarak yükle + Load… Yükle… + Localized Color Clusters Yerelleştirilmiş Renk Kümeleri + Lock Kilit + Lock all Tümünü kilitle + Lock image pixels Resim piksellerini kilitle + Lock layer Katmanı kilitle + Lock position Kilit konumu + Lock transparent pixels Şeffaf pikselleri kilitle + Lock: Kilit: + Logical Length: Mantıksal Uzunluk: + Logical Units: Mantıksal Birimler: + Luminance parlaklık + Luminance Detail Parlaklık Detayı + Magenta Macenta + Magenta · Green Macenta · Yeşil + Maintain aspect ratio En boy oranını koru + Marker Size İşaretleyici Boyutu + Mask All Tümünü Maskele + Mask Options Maske Seçenekleri + Masking Maskeleme + Master Usta + Matte Mat + Median: Medyan: + Menus Menüler + Mesh Size Örgü Boyutu + Metadata Meta veriler + Metrics Metrikler + Midpoint Orta nokta + Midtones Orta tonlar + Midtones from: Orta tonlar: + Midtones to: Orta tonlar: + Minimum Depth Minimum Derinlik + Minimum Diameter Minimum Çap + Minimum Roundness Minimum Yuvarlaklık + Mode: Mod: + Monochrome Tek renkli + More Points Daha Fazla Puan + Move effect layer down Efekt katmanını aşağı taşı + Move effect layer up Efekt katmanını yukarı taşı + Move to Şuraya taşı: + Name Ad + Name: Ad: + Nearest Neighbor En Yakın Komşu + Nearest Neighbor (hard edges) En Yakın Komşu (sert kenarlar) + New Size Yeni Boyut + New document… Yeni belge… + New effect layer Yeni efekt katmanı + New selection Yeni seçim + Next frame Sonraki kare + Next note Sonraki not + No Color Renk Yok + No Color Management Renk Yönetimi Yok + No Dither Titreşim Yok + No author Yazar yok + No character style Karakter stili yok + No document Belge yok + No matching commands Eşleşen komut yok + No presets for the current tool. Geçerli araç için ön ayar yok. + No presets of this type. Bu türden ön ayar yok. + No properties Mülk yok + No tool presets. Araç ön ayarı yok. + Noise Reduction Gürültü Azaltma + Normal Normal + Not in Dictionary: Sözlükte Yok: + OK Tamam + Offset Ofset + Only Web Colors Yalnızca Web Renkleri + Opacity Opaklık + Opacity Jitter Opaklık Değişimi + Opacity: Opaklık: + Open a document to define variables. Değişkenleri tanımlamak için bir belge açın. + Open a document to select a colour range. Bir renk aralığı seçmek için bir belge açın. + Open a document to use the timeline. Zaman çizelgesini kullanmak için bir belge açın. + Optical Optik + Optimized Optimize edilmiş + Option-click to define a source point to clone from Kopyalanacak bir kaynak noktası tanımlamak için Option tuşunu basılı tutarak tıklayın + Original Orijinal + Output Channel: Çıkış Kanalı: + Output Levels: Çıkış Seviyeleri: + Output: Çıkış: + Outside Dışarıda + Overlay Color Kaplama Rengi + Overlay Opacity Kaplama Opaklığı + Overlay: Rule of Thirds Kaplama: Üçte Bir Kuralı + Override Pressure Basıncı Geçersiz Kıl + Override Rotation Döndürmeyi Geçersiz Kıl + Override Tilt Eğimi Geçersiz Kıl + PRESET DETAILS ÖN AYAR DETAYLARI + PSD PSD + Paint Overlay Boya Kaplaması + Panel locations are saved in the workspace. Keyboard shortcuts, menus and toolbar are optional. Panel konumları çalışma alanına kaydedilir. Klavye kısayolları, menüler ve araç çubuğu isteğe bağlıdır. + Paper Kağıt + Path Yol + Pattern Desen + Pattern Size Desen Boyutu + Pause Duraklat + Percent Yüzde + Perceptual algısal + Perspective Warp is not active Perspektif Çarpıtma etkin değil + Perspective Warp: draw planes along the image's perspective, then switch to Warp Perspektif Çarpıtma: görüntünün perspektifi boyunca düzlemler çizin, ardından Çarpıtma'ya geçin + Photo Filter Fotoğraf Filtresi + PhotoCraft PhotoCraft + PhotoCraft Manages Colors PhotoCraft Renkleri Yönetir + PhotoCraft — an open-source, native image editor written in Rust. PhotoCraft — Rust'ta yazılmış açık kaynaklı, yerel bir resim düzenleyici. + Pin Depth: Pim Derinliği: + Pixel Length: Piksel Uzunluğu: + Pixels/Centimeter Piksel/Santimetre + Pixels/Inch Piksel/İnç + Pixels: Piksel: + Play Oyna + Play selection Seçimi oynat + Portrait Portre + Position Pozisyon + Position and Size Konum ve Boyut + Posterize Posterleştir + Preserve Details Ayrıntıları Koru + Preserve Luminosity Parlaklığı Koru + Preset Ön ayar + Preset Type Ön Ayar Türü + Preset: Ön ayar: + Presets: Ön ayarlar: + Press keys… Tuşlara basın… + Pressure Basınç + Pressure for Opacity Opaklık Basıncı + Pressure for Size Boyuta Göre Basınç + Prevent auto-nesting in and out of Artboards and Frames Çalışma Yüzeyleri ve Çerçevelerin içine ve dışına otomatik yerleştirmeyi önleyin + Preview Önizleme + Previous frame Önceki çerçeve + Previous note Önceki not + Print Yazdır + Printer Yazıcı + Printer Manages Colors Yazıcı Renkleri Yönetir + Printer Setup Yazıcı Kurulumu + Printing Marks Baskı İşaretleri + Progressive Aşamalı + Promote Tanıt + Protect Detail Detayı Koruyun + Protect Foreground Color Ön Plan Rengini Koruyun + Protect Tones Tonları Koruyun + Proximity Match Yakınlık Eşleşmesi + Pulled String Mode Çekilmiş Dize Modu + Puppet Warp is not active Kukla Çarpıtma etkin değil + Purity saflık + Quality Kalite + Quick Actions Hızlı Eylemler + Quick Export As PNG PNG Olarak Hızlı Dışa Aktarma + Radial Gradient Radyal Gradyan + Radius Yarıçap + Radius: Yarıçap: + Range: Aralık: + Rate Oran + Recently Used Son Kullanılanlar + Reconstruct Yeniden inşa et + Record ● a sequence of edits, then play ▶ it on any document. Bir dizi düzenlemeyi kaydedin ● ve ardından herhangi bir belgede oynatın. + Red Kırmızı + Redefine style by current selection Stili geçerli seçime göre yeniden tanımlayın + Refine Hassaslaştır + Reflected Gradient Yansıyan Degrade + Registration Marks Tescil İşaretleri + Regular Düzenli + Relative akraba + Relative Colorimetric Bağıl Kolorimetrik + Remove Kaldır + Remove All Pins Tüm Pinleri Kaldır + Remove Background Arka Planı Kaldır + Remove Planes Uçakları Kaldır + Remove stop Durağı kaldır + Remove variable Değişkeni kaldır + Rename Yeniden adlandır + Rename Group… Grubu Yeniden Adlandır… + Rename Layer… Katmanı Yeniden Adlandır… + Rename Tool Preset… Araç Ön Ayarını Yeniden Adlandır… + Rename… Yeniden adlandır… + Resample Yeniden örnekle + Reset All to Defaults Tümünü Varsayılanlara Sıfırla + Import Shortcuts… Kısayolları İçe Aktar… + Load a keyboard shortcut set saved as .kys .kys olarak kaydedilen bir klavye kısayolu kümesi yükleyin + Imported {n} shortcuts from {file}. Click OK to keep them. {n} kısayolları {file} adresinden içe aktarıldı. Bunları saklamak için Tamam'ı tıklayın. + {n} commands are not in PhotoCraft. {n} komutları PhotoCraft'ta değil. + Keyboard shortcuts imported İçe aktarılan klavye kısayolları + Your keyboard shortcut set {set} is in use here: {n} shortcuts differ from the defaults. Edit › Keyboard Shortcuts shows them; Reset All to Defaults undoes this. {set} klavye kısayol grubunuz burada kullanılıyor: {n} kısayolları varsayılanlardan farklı. Düzenle › Klavye Kısayolları bunları gösterir; Tümünü Varsayılanlara Sıfırla bu işlemi geri alır. + Import keyboard shortcuts? Klavye kısayolları içe aktarılsın mı? + PhotoCraft found your keyboard shortcut set {set} from another image editor on this computer: {n} of its shortcuts differ from PhotoCraft's. Import it? PhotoCraft, {set} klavye kısayol setinizi bu bilgisayardaki başka bir resim düzenleyicide buldu: {n} kısayolları PhotoCraft'ınkilerden farklı. İçe aktarılsın mı? + Edit › Keyboard Shortcuts › Reset All to Defaults undoes an import. Düzenle › Klavye Kısayolları › Tümünü Varsayılanlara Sıfırla, içe aktarma işlemini geri alır. + Don't Import İçe Aktarma + The keyboard shortcut set was not imported: {error} Klavye kısayol seti içe aktarılmadı: {error} + Reset Section Bölümü Sıfırla + Reset Transform Dönüşümü Sıfırla + Reset to defaults Varsayılanlara sıfırla + Resolution Çözünürlük + Resolution: Çözünürlük: + Restore All Tümünü Geri Yükle + Restore Defaults Varsayılanları Geri Yükle + Restrictive (Web) Kısıtlayıcı (Web) + Right align text Metni sağa hizala + Rigid Sert + Rotate: Döndür: + Rotation Döndürme + Roughness Pürüzlülük + Roundness Yuvarlaklık + Roundness Jitter Yuvarlaklık Değişimi + Rulers & Grids Cetveller ve Izgaralar + Sample All Layers Tüm Katmanları Örnekle + Sample: Örnek: + Sampling: Background Swatch Örnekleme: Arka Plan Renk Örneği + Sampling: Continuous Örnekleme: Sürekli + Sampling: Once Örnekleme: Bir kez + Saturate Doygunluk + Saturation Doygunluk + Saturation Jitter Doygunluk Değişimi + Save HTML and Images HTML ve Görselleri Kaydet + Save as PDF PDF olarak kaydet + Save selection as channel Seçimi kanal olarak kaydet + Save… Kaydet… + Scale to Fit Media Medyaya Sığacak Şekilde Ölçeklendirin + Scatter Dağılım + Scratch disks Kazıma diskleri + Search Ara + Search commands Arama komutları + Search commands, tools and panels… Komutları, araçları ve panelleri arayın… + Search fonts Yazı tiplerini arayın + Select All Tümünü Seç + Select Subject Konuyu Seçin + Select a type layer or click into text to insert glyphs Bir yazım katmanı seçin veya glif eklemek için metne tıklayın + Select text to apply a character style Karakter stili uygulamak için metni seçin + Select: Seçin: + Selections Seçimler + Selective seçici + Selective Color Seçici Renk + Send the selected pin backward Seçilen pini geriye gönder + Set additional smoothing options Ek yumuşatma seçeneklerini ayarlayın + Set background color Arka plan rengini ayarla + Set foreground color Ön plan rengini ayarla + Set painting symmetry options Boyama simetrisi seçeneklerini ayarlama + Symmetry Off Simetri Kapalı + Set shape fill type Şekil doldurma türünü ayarla + Set shape stroke type Şekil kontur türünü ayarlama + Set the text color Metin rengini ayarlama + Shadows Gölgeler + Shape: Şekil: + Sharpening Bileme + Shift Shift + Shortcut removed. Kısayol kaldırıldı. + Show All Menu Items Tüm Menü Öğelerini Göster + Show Backdrop Arka Planı Göster + Show Guides Kılavuzları Göster + Show Mask Maskeyi Göster + Show Mesh Mesh'i Göster + Show Overlay Kaplamayı Göster + Show Transform Controls Dönüştürme Kontrollerini Göster + Show or hide the Notes panel Notlar panelini gösterme veya gizleme + Show: Göster: + Sides Taraflar + Sides: Taraflar: + Size Boyut + Size Jitter Boyut Değişimi + Size: Boyut: + Slice Options… Dilim Seçenekleri… + Slices From Guides Kılavuzlardan Dilimler + Slices are locked (View › Lock Slices) Dilimler kilitlidir (Görünüm › Dilimleri Kilitle) + Small Caps Küçük Büyük Harfler + Smoothing Pürüzsüzleştirme + Space After Sonraki Boşluk + Space Before Önceki Boşluk + Spacing Aralık + Spell check complete. Yazım denetimi tamamlandı. + Split: Bölünmüş: + Star ratio Yıldız oranı + Stop playing/recording Oynatmayı/kaydetmeyi durdur + Straighten Düzelt + Straighten Layer Katmanı Düzleştir + Strength: Güç: + Strikethrough Üstü çizili + Stroke Kontur + Stroke Catch-up İnme Yakalama + Stroke: İnme: + Style Stil + Style Options Stil Seçenekleri + Style Options… Stil Seçenekleri… + Style: Normal Tarz: Normal + Subtract from Sample Örnekten Çıkart + Subtract from selection ({key}) Seçimden çıkar ({key}) + Suggestions: Öneriler: + Swap colours (X) Renkleri değiştir (X) + Swaps height and width Yüksekliği ve genişliği değiştirir + Switch between free transform and warp modes Serbest dönüştürme ve çarpıtma modları arasında geçiş yapın + Switch theme Temayı değiştir + Temperature Sıcaklık + Tetrahedral dörtyüzlü + Text Position: Metin Konumu: + Texture Doku + Texture Each Tip Her Ucu Doku + Threshold Eşik + Threshold Level Eşik Seviyesi + Thumbnail size (smallest: list) Küçük resim boyutu (en küçük: liste) + Tilt X X'i yatır + Tilt Y Y Eğimi + Tint Renk tonu + Toggle count group visibility Sayım grubu görünürlüğünü değiştir + Toggle text orientation Metin yönünü değiştir + Tolerance Tolerans + Tolerance: Tolerans: + Tone: Ton: + Toolbar Araç Çubuğu + Top Üst + Total: Toplam: + Tracking Takip + Tracking (1/1000 em) Takip (1/1000 em) + Transparency Şeffaflık + Transparent Şeffaf + Trilinear Üç çizgili + Type Styles Tip Stilleri + Type: Tür: + Underline Altı çizili + Untitled İsimsiz + Untitled-1 İsimsiz-1 + Update layer comp (visibility, position and appearance) Katman kompozisyonunu güncelleyin (görünürlük, konum ve görünüm) + Use Default Varsayılanı Kullan + Use Legacy Eskiyi Kullan + Use Measurement Scale Ölçüm Ölçeğini Kullan + Use the embedded profile (instead of the working space) Gömülü profili kullanın (çalışma alanı yerine) + Variables bind a layer's visibility, text or pixels to a named data slot. Değişkenler bir katmanın görünürlüğünü, metnini veya piksellerini adlandırılmış bir veri yuvasına bağlar. + Vertical scale Dikey ölçek + Vibrance Canlılık + View Options Seçenekleri Görüntüle + Vignetting Kenar ayrıntısı kaybı + Violet, Orange Menekşe, Turuncu + Visibility Görünürlük + Visible Görünür + Warp: Çözgü: + Web Snap Web Snap + Weight Ağırlık + Weight: Ağırlık: + White Beyaz + White Balance: As Shot Beyaz Dengesi: Çekildiği Gibi + Whites Beyazlar + Width Genişlik + Slant Eğimli + Width: Genişlik: + Work Path Çalışma Yolu + Yellow Sarı + Yellow · Blue Sarı · Mavi + Zoom Yakınlaştır + Zoom in Yakınlaştır + Zoom out Uzaklaştır + command or shortcut komut veya kısayol + current akım + image file path resim dosyası yolu + modified değiştirilmiş + name isim + new yeni + {name} options {name} seçenekler + ↑↓ navigate {key} run esc close ↑↓ gezin {key} çalıştır esc kapat + Version {version} Sürüm {version} + New Document Yeni Belge + New Document With Mask Maskeli Yeni Belge + New Layer With Mask Maskeli Yeni Katman + Save Preset… Ön Ayarı Kaydet… + Preset Name Ön Ayar Adı + No saved presets yet. Henüz kayıtlı ön ayar yok. + Error Hata + Command Komut + Adjustment layers have nothing to transform Ayarlama katmanlarının dönüştürülecek hiçbir şeyi yok + Altitude Rakım + Android 1080p Android1080p + Apple Watch 45mm Apple Watch 45mm + Aqua Su + Art & Illustration Sanat ve İllüstrasyon + ArtCraft ArtCraft + Background Eraser Tool Arka Plan Silgisi Aracı + Bevel & Emboss Eğim Ver ve Kabart + Blend Mode Karışım Modu + Blues Blues + Blur Tool Bulanıklık Aracı + Bold Kalın + Brush Fırça + Block Blok + Brush Pose Fırça Duruşu + Brush Tip Shape Fırça Ucu Şekli + Brush Tool Fırça Aracı + Build-up İnşaat + Burn Tool Koyulaştırma Aracı + Centimeters Santimetre + Choke Şok + Classic Klasik + Click to add text Metin eklemek için tıklayın + Click to fill similar colours Benzer renkleri doldurmak için tıklayın + Clone Stamp Tool Klonlama Damgası Aracı + Color Burn Renk Yanması + Color Dodge Renk Soldurma + Color Dynamics Renk Dinamiği + Color Overlay Renk Kaplaması + Comic Book Çizgi Roman + Conform Uygun + Content-Aware Fill needs a selection on a pixel layer İçeriğe Uygun Dolgu, piksel katmanında seçim yapılmasını gerektirir + Could not transform: the layer is empty Dönüştürülemedi: katman boş + Could not use Perspective Warp: the layer is empty Perspektif Çarpıtma kullanılamadı: katman boş + Could not use Puppet Warp: no opaque pixels Puppet Warp kullanılamadı: opak piksel yok + Could not use Puppet Warp: the layer is empty Puppet Warp kullanılamadı: katman boş + Crop Tool Kırpma Aracı + Crosswise Çapraz olarak + Current Tool Güncel Araç + Custom Shape Tool Özel Şekil Aracı + Cyans Camgöbeği + Default PhotoCraft Size Varsayılan PhotoCraft Boyutu + Delete Workspace Çalışma Alanını Sil + Diamond Elmas + Dimensions: Boyutlar: + Direction Yön + Distance Uzaklık + Document Dimensions Belge Boyutları + Document Profile Belge Profili + Document Sizes Belge Boyutları + Dodge Tool Açıklaştırma Aracı + Down Aşağı + Drag to draw a gradient Degrade çizmek için sürükleyin + Drag to move the active layer Etkin katmanı taşımak için sürükleyin + Drag to pan · hold Space with any tool Kaydırmak için sürükleyin · herhangi bir araçla Space tuşunu basılı tutun + Drop Shadow Alt Gölge + Dual Brush Çift Fırça + Edge Kenar + Efficiency Verimlilik + Efficiency: 100% Verimlilik: %100 + Ellipse Tool Elips Aracı + Elliptical Marquee Tool Eliptik İşaretleme Aracı + Single Row Marquee Tool Tek Satır İşaretleme Aracı + Single Column Marquee Tool Tek Sütun İşaretleme Aracı + Emboss Kabartma + Eraser Silgi + Eraser Tool Silgi Aracı + ExtraBold Ekstra Kalın + ExtraLight Ekstra Hafif + Eyedropper Tool Damlalık Aracı + Fade Solmaya + Fill Opacity Dolgu Opaklığı + Film & Video Film ve Video + From Gönderen + General Genel + GitHub GitHub + Gradient Overlay Degrade Yerleşimi + Gradient Tool Degrade Aracı + Greens Yeşiller + Hand Tool El Aracı + Hard Sert + Hard Mix Sert Karışım + Healing Brush Tool Düzeltme Fırçası Aracı + Highlights from: Öne çıkanlar: + History Brush Tool Geçmiş Fırçası Aracı + Horizontal Type Tool Yatay Metin Aracı + Horizontal Type Mask Tool Yatay Metin Maskesi Aracı + Vertical Type Mask Tool Dikey Metin Maskesi Aracı + Image Size: Resim Boyutu: + Inches İnç + Indexed Dizine eklendi + Initial Direction Başlangıç Yönü + Inner Bevel İç Eğim + Inner Glow İç Işıma + Inner Shadow İç Gölge + Lab Lab + Landscape, 10 x 8 Yatay, 10 x 8 + Landscape, 6 x 4 Yatay, 6 x 4 + Landscape, 7 x 5 Yatay, 7 x 5 + Lasso Kement + Lasso Tool Kement Aracı + Layer Count Katman Sayısı + Layer Knocks Out Drop Shadow Katman Alt Gölgeyi Ortadan Kaldırır + Legal Yasal + Letter Mektup + Light Işık + Line Tool Çizgi Aracı + Linear Doğrusal + Linear Burn Doğrusal Yanma + Linear Height Doğrusal Yükseklik + Link with Layer Katmanla Bağlantı + Lorem Ipsum Lorem İpsum + Magentas Macentalar + Magic Eraser Tool Sihirli Silgi Aracı + Magic Wand Tool Sihirli Değnek Aracı + Marquee Seçim Çerçevesi + Mean: Anlamı: + Measurement Scale Ölçüm Ölçeği + Measurement Scale Marker Ölçüm Ölçeği İşaretleyicisi + Millimeters Milimetre + Mobile Mobil + Move Tool Taşıma Aracı + Could not use the move tool because the layer is locked. Katman kilitli olduğundan taşıma aracı kullanılamadı. + Could not complete the Free Transform command because the layer is locked. Katman kilitli olduğundan Serbest Dönüştürme komutu tamamlanamadı. + Multiply Çoğalt + My Brushes Fırçalarım + Neutrals Nötrler + New Workspace Yeni Çalışma Alanı + Note Tool Not Aracı + Object Selection Tool Nesne Seçme Aracı + Off Kapalı + Open Aç + Orange Turuncu + Original Ratio Orijinal Oran + Out Of Gamut Gamut Dışı + Outer Bevel Dış Eğim + Outer Glow Dış Parıltı + Overlay Kaplama + Paint Bucket Tool Boya Kovası Aracı + Paints from the document's opening state Belgenin açılış durumundaki boyalar + Path Selection Tool Yol Seçimi Aracı + Direct Selection Tool Doğrudan Seçim Aracı + Add Anchor Point Tool Bağlantı Noktası Ekleme Aracı + Delete Anchor Point Tool Bağlantı Noktası Silme Aracı + Click a segment to add an anchor point · {alt}-click an anchor to delete it Bağlantı noktası eklemek için bir segmenti tıklayın · {alt}-silmek için bir bağlantıyı tıklayın + Click an anchor point to delete it · {alt}-click a segment to add one Silmek için bir bağlantı noktasını tıklayın · {alt}-bir parça eklemek için bir parçayı tıklayın + Pattern Overlay Desen Yerleşimi + Pen Pressure Kalem Basıncı + Pen Tilt Kalem Eğimi + Pen Tool Kalem Aracı + Convert Point Tool Nokta Dönüştürme Aracı + Photo Fotoğraf + PhotoCraft website PhotoCraft web sitesi + Picas Pikalar + Pillow Emboss Yastık Kabartma + Pixel Replacement Piksel Değiştirme + Pixels Piksel + Points Puanlar + Polygon Tool Çokgen Aracı + Polygonal Lasso Tool Çokgen Kement Aracı + Portrait, 4 x 6 Portre, 4 x 6 + Portrait, 5 x 7 Portre, 5 x 7 + PostScript (72 points/inch) PostScript (72 nokta/inç) + Postcard Kartpostal + Poster Poster + Pro (Dark) Pro (Koyu) + Pro (Light) Pro (Hafif) + Pro (Light Gray) Pro (Açık Gri) + Pro (Medium Gray) Pro (Orta Gri) + Protect Texture Dokuyu Koruyun + Purple Mor + Quick Mask Hızlı Maske + Quick Selection Tool Hızlı Seçim Aracı + Radial Radyal + Rasterize Layer Katmanı Rasterleştir + Rasterize Layers Katmanları Rasterleştir + Ratio Oran + Recent Son + Rectangle Tool Dikdörtgen Aracı + Rectangular Marquee Tool Dikdörtgen İşaretleme Aracı + Reds Kırmızılar + Reflected Yansıyan + Sample: foreground colour Örnek: ön plan rengi + Sampled Colors Örneklenen Renkler + Satin Saten + Save Pixel Aspect Ratio Piksel En Boy Oranını Kaydet + Scattering Saçılma + Scratch Sizes Çizik Boyutları + Select Data Points Veri Noktalarını Seçin + Selection Seçim + SemiBold Yarı Kalın + Shadows up to: Şuna kadar gölgeler: + Shape Dynamics Şekil Dinamiği + Sharpen Tool Keskinleştirme Aracı + Show Extras Options Ekstra Seçeneklerini Göster + Slice Select Tool Dilim Seçme Aracı + Slice Tool Dilim Aracı + Smart Filters Akıllı Filtreler + Smudge Tool Leke Aracı + Soft Yumuşak + Soften Yumuşat + Source Kaynak + Source: set Kaynak: ayarlandı + Sponge Tool Sünger Aracı + Spot Healing Brush Tool Nokta Düzeltme Fırçası Aracı + Spread Yayılma + Square, 12 x 12 Kare, 12x12 + Square, 5 x 5 Kare, 5x5 + Std Dev: Standart Geliştirme: + Studio (Dark) Stüdyo (Karanlık) + Studio (Light) Stüdyo (Işık) + Stylus Wheel Kalem Tekerleği + Subtract Çıkar + Tabloid Tabloid + Text Replacement Metin Değiştirme + Thin İnce + Traditional (72.27 points/inch) Geleneksel (72,27 nokta/inç) + Transfer Aktarım + Triangle Tool Üçgen Aracı + Unsaved changes Kaydedilmemiş değişiklikler + Uses the foreground and background colours. Ön plan ve arka plan renklerini kullanır. + Web ağ + Web Large Web Büyük + Web Minimum Web Minimumu + Web Most Common Web En Yaygın + WebP (lossless) WebP (kayıpsız) + Wet Edges Islak Kenarlar + Yellows Sarılar + Zoom Tool Yakınlaştırma Aracı + {name} Properties {name} Özellikler + Revert “{name}” to the last saved version? Your changes will be lost. “{name}” son kaydedilen sürüme döndürülsün mü? Değişiklikleriniz kaybolacak. + Do you want to save the changes you made to “{name}” before quitting? Çıkmadan önce “{name}” öğesinde yaptığınız değişiklikleri kaydetmek istiyor musunuz? + Do you want to save the changes you made to “{name}” before closing? Kapatmadan önce “{name}” üzerinde yaptığınız değişiklikleri kaydetmek istiyor musunuz? + Doc: {flat}/{layered} Doküman: {flat}/{layered} + Scratch: {size} Çizik: {size} + Untagged {mode} Etiketlenmemiş {mode} + Invalid {mode} profile Geçersiz {mode} profili + Unnamed {mode} profile Adsız {mode} profili + {w} px x {h} px ({ppi} ppi) {w} px x {h} px ({ppi} ppi) + {w} {unit} x {h} {unit} ({ppi} ppi) {w} {unit} x {h} {unit} ({ppi} ppi) + Resolution: {dpi} pixels/inch Çözünürlük: {dpi} piksel/inç + Doc: {w} × {h} px Doküman: {w} × {h} piksel + Frame {current} / {total} Çerçeve {current} / {total} + Count: {n} Sayım: {n} + BLANK DOCUMENT PRESETS ({n}) BOŞ BELGE ÖN AYARLARI ({n}) + Constraints ({n}) Kısıtlamalar ({n}) + Align {what} {what} hizala +@plural {n} Layer|{n} Layers {n} Katman +@plural {n} layer|{n} layers {n} katman +@plural Group · {n} layer|Group · {n} layers Grup · {n} katman + Additional plugins folder Ek eklentiler klasörü + Adobe Adobe + Adobe rgb Adobe rgb + Allow agent control Aracı kontrolüne izin ver + Allow scripts to connect Komut dosyalarının bağlanmasına izin ver + Always Her zaman + Always create smart objects when placing Yerleştirirken daima akıllı nesneler yaratın + Animated zoom Animasyonlu yakınlaştırma + Apply auto tone Otomatik tonu uygula + Ask Sor + Ask before saving layered tiff Katmanlı tiff'i kaydetmeden önce sor + Auto collapse icon panels Simge panellerini otomatik daralt + Auto show hidden panels Gizli panelleri otomatik göster + Auto show home screen Ana ekranı otomatik göster + Autosave Otomatik kaydetme + Autosave minutes Dakikaları otomatik kaydet + Beep when done Bittiğinde bip sesi + Bicubic automatic Bikübik otomatik + Bicubic sharper Bikübik daha keskin + Bicubic smoother Bikübik daha pürüzsüz + Bit depth Bit derinliği + Brush preview color Fırça önizleme rengi + Cache levels Önbellek düzeyleri + Cache tile size Önbellek kutucuğu boyutu + Camera Raw Defaults Camera Raw Varsayılanları + Canvas border Kanvas kenarlığı + Show canvas dots Kanvas noktalarını göster + Canvas color Kanvas rengi + Canvas custom color Kanvas özel rengi + Color picker Renk seçici + Color space Renk alanı + Column width Sütun genişliği + Concise Kısa + Control port Kontrol portu + Convert to srgb srgb'ye dönüştür + Cursors İmleçler + Custom dark Özel koyu + Custom light Özel ışık + Dark Karanlık + Dark gray Koyu gri + Dashed lines Kesikli çizgiler + Destination Hedef + Detailed Detaylı + Disks Diskler + Display p3 Ekran p3 + Double click layer mask launches select and mask Çift tıklama katman maskesi seçme ve maskelemeyi başlatır + Drop shadow Alt gölge + Dynamic color sliders Dinamik renk kaydırıcıları + East asian Doğu Asya + Effect cache mb Efekt önbelleği mb + Enable flick panning Hızlı kaydırmayı etkinleştir + Enable floating document window docking Kayan belge penceresi yerleştirmeyi etkinleştir + Enable narrow options bar Dar seçenekler çubuğunu etkinleştir + Enabled Etkin + Enhanced Controls Gelişmiş Kontroller + Export clipboard Panoyu dışa aktar + Extra large Ekstra büyük + File Handling Dosya İşleme + File path Dosya yolu + Fill new type layers with placeholder Yeni yazı katmanlarını yer tutucuyla doldurma + Font preview Yazı tipi önizlemesi + Full size tip Tam boyutlu uç + Gamut warning color Gamut uyarı rengi + Gamut warning opacity Gamut uyarı opaklığı + Generator enabled Jeneratör etkin + Gif Gif + Glossy parlak + Grid color Izgara rengi + Grid colors Izgara renkleri + Grid size Izgara boyutu + Grid style Izgara stili + Grid unit Izgara ünitesi + Gridline every Kılavuz çizgisi her + Guide color Kılavuz rengi + Guide style Kılavuz stili + Guides, Grid & Slices Kılavuzlar, Izgaralar ve Dilimler + Gutter Oluk + History Log Geçmiş Günlüğü + History states Tarih durumları + Ignore exif profile tag Exif profil etiketini yoksay + Image interpolation Görüntü enterpolasyonu + Image previews Resim önizlemeleri + Integrations Entegrasyonlar + Interface Arayüz + Jpeg quality JPEG kalitesi + Jpg Jpg + Language Dil + Large tabs Büyük sekmeler + Legacy compositing Eski birleştirme + Light gray Açık gri + Line Hat + Lines çizgiler + Lossless Kayıpsız + Lowercase extension Küçük harf uzantısı + Maximize psd compatibility PSD uyumluluğunu en üst düzeye çıkarın + Medium gray Orta gri + Memory usage mb Bellek kullanımı mb + Low resolution previews Düşük çözünürlüklü önizlemeler + Missing glyph protection Glif koruması eksik + Modern Çağdaş + Nearest neighbor En yakın komşu + Never Asla + Normal tip Normal uç + Open as smart object Akıllı nesne olarak aç + Open in camera raw Camera raw'da aç + Open documents as tabs Belgeleri sekmeler halinde aç + Overscroll Aşırı kaydırma + Performance Performans + Png png + Point size Nokta boyutu + Precise Hassas + Preserve details Ayrıntıları koru + Print resolution Yazdırma çözünürlüğü + Pro Profesyonel + Pro light Profesyonel ışık + Pro light gray Pro açık gri + Pro medium Profesyonel orta + Pro photo Profesyonel fotoğraf + Quick export format Hızlı dışa aktarma formatı + Quick export location Hızlı dışa aktarma konumu + Recent file count Son dosya sayısı + Recent files Son dosyalar + Recent fonts En yeni yazı tipleri + Recover on launch Başlatıldığında kurtar + Remember workspace changes Çalışma alanı değişikliklerini hatırla + Resize image during place Yerleştirme sırasında resmi yeniden boyutlandır + Rotate view with trackpad İzleme dörtgeniyle görünümü döndürme + Same folder Aynı klasör + Save in background Arka planda kaydet + Scratch Disks Kazı Diskleri + Screen Ekran + Screen resolution Ekran çözünürlüğü + Scrubby slider acceleration Temiz kaydırıcı hızlandırması + Sessions only Yalnızca oturumlar + Sharpen for Şunun için keskinleştir: + Show channels in color Kanalları renkli göster + Show crosshair in brush tip Fırça ucunda artı işaretini göster + Show extension panels Uzatma panellerini göster + Show font names in english İngilizce yazı tipi adlarını göster + Show menu colors Menü renklerini göster + Show only crosshair while painting Boyama sırasında yalnızca artı işaretini göster + Show slice numbers Dilim numaralarını göster + Show tooltips Araç ipuçlarını göster + Show highlight on rollover Rollover sırasında vurgulamayı göster + Show bounding box when dragging layer Katmanı sürüklerken sınırlayıcı kutuyu göster + System title bar Sistem başlık çubuğu + Show transformation values Dönüşüm değerlerini göster + Slice color Dilim rengi + Smart guide color Akıllı kılavuz rengi + Smart quotes Akıllı tırnaklar + Snap vector tools and transforms to pixel grid Vektör araçlarını tutturun ve piksel ızgarasına dönüştürün + Srgb Srgb + Standard Standart + Studio Stüdyo + Studio light Stüdyo ışığı + Subdivisions Alt bölümler + System Sistem + Text engine Metin motoru + Text file Metin dosyası + Tiny minik + Touch gestures Dokunma hareketleri + Transparency & Gamut Şeffaflık ve Gamut + Type units Tip birimleri + UI font size kullanıcı arayüzü yazı tipi boyutu + UI scale Kullanıcı arayüzü ölçeği + Units & Rulers Birimler ve Cetveller + Use additional plugins folder Ek eklentiler klasörünü kullan + Use esc to commit Taahhüt etmek için esc kullanın + Use gpu GPU'yu kullan + Use legacy free transform Eski ücretsiz dönüşümü kullanın + Use shift key for tool switch Araç anahtarı için Shift tuşunu kullanın + Vary round brush hardness on hud Hud üzerinde yuvarlak fırça sertliğini değiştirin + Webp Webp + World ready Dünya hazır + Zoom clicked point to center Tıklanan noktayı merkeze yakınlaştır + Zoom resizes windows Yakınlaştırma pencereleri yeniden boyutlandırır + Zoom with scroll wheel Kaydırma tekerleği ile yakınlaştırma + Zoom with trackpad pinch İzleme dörtgeni tutamıyla yakınlaştırma + Pass Through Geçirgen + Dissolve Eritme + Darker Color Daha Koyu Renk + Linear Dodge (Add) Doğrusal Soldurma (Ekle) + Lighter Color Daha Açık Renk + Soft Light Yumuşak Işık + Hard Light Sert Işık + Vivid Light Canlı Işık + Linear Light Doğrusal Işık + Pin Light Nokta Işığı + Exclusion Dışlama + Divide Böl + Luminosity Parlaklık +@plural {n} sample|{n} samples {n} örnek + {added}, {removed} subtracted {added}, {removed} çıkarıldı + Adjustment Preview Ayar Önizlemesi + Airbrush: keeps depositing paint while the pointer rests. Airbrush: İşaretçi dururken boyayı bırakmaya devam eder. + Angle: Açı: + Blend If Eğer karıştır + Close Tab Group Sekme Grubunu Kapat + Collapse Daralt + Collapse All Groups Tüm Grupları Daralt + Collapse Panel Group Panel Grubunu Daralt + Collapse group ({key}-click: all groups) Grubu daralt ({key}-tıklama: tüm gruplar) + Create new brush from the current settings Mevcut ayarlardan yeni fırça oluştur + Create new brush from these settings Bu ayarlardan yeni fırça oluşturun + Update the current brush with these settings Mevcut fırçayı bu ayarlarla güncelleyin + Delete brush Fırçayı sil + Drag the arrow to set the angle, the dots to set the roundness Açıyı ayarlamak için oku, yuvarlaklığı ayarlamak için noktaları sürükleyin + Expand Genişlet + Expand Panel Group Panel Grubunu Genişlet + Expand group ({key}-click: all groups) Grubu genişlet ({key}-tıklayın: tüm gruplar) + Fade steps Adımları soldur + Hide the layer's effects ({key}-click: all layers) Katmanın efektlerini gizle ({key}-tıklama: tüm katmanlar) + Import Brushes… (.abr) Fırçaları İçe Aktar… (.abr) + Move Group Down Grubu Aşağı Taşı + Move Group Up Grubu Yukarı Taşı + Reset the brush to the defaults Fırçayı varsayılanlara sıfırlayın + Restore original size Orijinal boyutu geri yükle + Roundness: Yuvarlaklık: + Search Brushes Fırçalarda Ara + Show the layer's effects ({key}-click: all layers) Katmanın efektlerini göster ({key}-tıklama: tüm katmanlar) + Brush picker Fırça seçici + Erase Sil + Right click with painting tools Boyama araçlarıyla sağ tıklayın + System Info… Sistem Bilgisi… + System Info Sistem Bilgisi + More panels Daha fazla panel + More documents Daha fazla belge + These settings aren't available in PhotoCraft yet. Bu ayarlar henüz PhotoCraft'ta mevcut değildir. + Graphics Grafik + Reset GPU Backend GPU Arka Uçunu Sıfırla + Applies at next launch. Bir sonraki açılışta geçerlidir. + Click to sample the foreground colour · {key}-click for background Ön plan rengini örneklemek için tıklayın · {key}-arka plan için tıklayın + {key}-click to set the source {key}-kaynağı ayarlamak için tıklayın + {key} to subtract {key} çıkarmak için + Clone source {n} ({key}-click with Clone Stamp to set) Kaynağı kopyala {n} ({key}-ayarlamak için Klonlama Damgası ile tıklayın) + Source: not set ({key}-click with Clone Stamp) Kaynak: ayarlanmadı ({key}-Klon Damgası ile tıklayın) + Pixel Layer Piksel Katmanı + Type Layer Tip Katmanı + Shape Layer Şekil Katmanı + Group Layer Grup Katmanı + Fill Layer Dolgu Katmanı + Smart Object Layer Akıllı Nesne Katmanı + Create Warped Text Çarpık Metin Oluştur + Rasterize Smart Object Akıllı Nesneyi Rasterleştir + Rasterize Fill Content Dolgu İçeriğini Rasterleştir + Rasterize Layer Style Katman Stilini Rasterleştir + Unlock: picking a preset replaces these settings Kilidi aç: bir ön ayarın seçilmesi bu ayarların yerini alır + Lock: keep these settings when picking another preset Kilitle: başka bir ön ayar seçerken bu ayarları koruyun + Diameter: Çap: + Hardness: Sertlik: + No brush presets Fırça ön ayarı yok + Toggle the Brush Settings panel (F5) Fırça Ayarları panelini açın/kapatın (F5) + Create new brush preset from the current settings Geçerli ayarlardan yeni fırça hazır ayarı oluşturun + Brush Settings… (F5): every option of this brush Fırça Ayarları… (F5): bu fırçanın tüm seçenekleri + Rename Brush… Fırçayı Yeniden Adlandır… + Delete Brush Fırçayı Sil + New Brush Preset… Yeni Fırça Ön Ayarı… + Brush Preset Options Fırça Ön Ayarı Seçenekleri + Group name Grup adı + Brush name Fırça adı + Brush Name Fırça Adı + Off: the speed of the pointer sets the spacing Kapalı: işaretçinin hızı aralığı belirler + Tilt Scale Eğim Ölçeği + Brush Projection Fırça Projeksiyonu + Apply the pen's tilt and rotation to the tip shape Kalemin eğimini ve dönüşünü uç şekline uygulayın + Mixer Brush Karıştırıcı Fırça + Mixer Brush Tool Karıştırıcı Fırça Aracı + Color Replacement Tool Renk Değiştirme Aracı + Wet Islaklık + Mix Karışım + Wetness Jitter Islaklık Titreme + Mix Jitter Karıştırma Titreşimi + Show the layer mask as an overlay Katman maskesini kaplama olarak göster + Toggle visibility Görünürlüğü aç/kapat + Unlink the mask from the layer Maskenin katmanla bağlantısını kaldırın + Link the mask to the layer Maskeyi katmana bağlama + Gradient Gradyan + Gradient Options Degrade Seçenekleri + Align with layer Katmanla hizala + Reset Alignment Hizalamayı Sıfırla + Centre the gradient (offset 0, 0) Degradeyi ortalayın (kaydırma 0, 0) + Gradient Editor Degrade Düzenleyici + Gradient picker Degrade seçici + Gradient stops Gradyan durakları + Save As Preset Ön Ayar Olarak Kaydet + The gradient the Gradient tool paints with Degrade aracının boyadığı degrade + Adds the current gradient to the Gradients panel Geçerli degradeyi Degradeler paneline ekler + The current gradient (pick one in Window › Gradients) Geçerli degrade (Pencere › Degradeler'den birini seçin) + Select a type layer to edit its character and paragraph settings. Karakter ve paragraf ayarlarını düzenlemek için bir yazı katmanı seçin. + Text color Metin rengi + Type Options Tip Seçenekleri + Anti-aliasing method Kenar yumuşatma yöntemi + Horizontal type Yatay tip + Vertical type Dikey tip + Fill path with foreground color Yolu ön plan rengiyle doldur + Stroke path with brush Fırçayla kontur yolu + Load path as a selection Yolu seçim olarak yükle + Make work path from selection Seçimden iş yolunu yap + Add vector mask Vektör maskesi ekle + Create new path Yeni yol oluştur + Delete path Yolu sil + Delete vector mask Vektör maskesini sil + CPU (no GPU acceleration) CPU (GPU hızlandırması yok) + DirectX 12 DirectX 12 + GPU backend GPU arka ucu + Linux display server Linux görüntü sunucusu + X11 X11 + Metal Metal + OpenGL OpenGL + Use tablet pressure Tablet basıncını kullan + Vulkan Vulkan + Alt-click to define a source point to clone from Kopyalanacak kaynak noktayı tanımlamak için Alt tuşuna basarak tıklayın + No path to make a selection from: draw one with the Pen tool or select one in the Paths panel. Seçim yapılabilecek yol yok: Kalem aracıyla bir yol çizin veya Yollar panelinden birini seçin. + Cancel opening Açılışı iptal et + Cancelled opening {name} {name} açılışı iptal edildi + Developing {name} {name} geliştiriliyor + Cancelled {label} {label} iptal edildi + Esc to cancel İptal etmek için Esc + Opened {name} {name} tarihinde açıldı + Opening {name} {name} açılıyor + Saved {name} {name} kaydedildi + Saving {name} {name} kaydediliyor + A save to {name} is still running; try again when it finishes {name} konumuna kaydetme işlemi hâlâ devam ediyor; bittiğinde tekrar dene + {label} is writing the file and can no longer be cancelled {label} dosyayı yazıyor ve artık iptal edilemez + Opening {name}… {name} açılıyor… + Opening… Açılıyor… + Reading… Okunuyor… + Working… Çalışıyor… + Color Range Renk Aralığı + Adaptive Wide Angle Uyarlanabilir Geniş Açı + Chalk & Charcoal Tebeşir ve Kömür + Charcoal Kömür + Chrome Krom + Conté Crayon Conte Crayon + Graphic Pen Grafik Kalemi + Halftone Pattern Noktalı Resim Deseni + Note Paper Not Kağıdı + Photocopy Fotokopi + Plaster Alçı + Reticulation Retikülasyon + Stamp Damga + Torn Edges Yırtık Kenarlar + Water Paper Su Kağıdı + Move Taşı + Pencil Kalem + Clone Stamp Klon Damgası + Dodge Dodge + Burn Yanık + Quick Selection Hızlı Seçim + History Brush Tarih Fırçası + Color Replacement Renk Değiştirme + New Group Yeni Grup + Layer Comp Katman Kompozisyonu + Alpha Alfa + Make Yap + Model Modeli + F F + Artist Sanatçı + Spot Color Nokta Rengi + Roman Roma + Bold Italic Kalın İtalik + Justification Gerekçe + Symbols Semboller + Punctuation Noktalama işaretleri + Make Work Path İş Yolu Yap + Make Selection Seçim Yap + Fill Path Yolu Doldur + Stroke Path Kontur Yolu + Delete Path Yolu Sil + W W + H H + Ruler Cetvel + New Layer Comp Yeni Katman Kompozisyonu + Duplicate Layer Comp Yinelenen Katman Kompozisyonu + Rename Layer Comp Katman Kompozisyonunu Yeniden Adlandırın + Apply Layer Comp Katman Kompozisyonunu Uygula + Update Layer Comp Katman Kompozisyonunu Güncelle + Method Yöntem + Author Yazar + Software Yazılım + Pencil Tool Kurşun Kalem Aracı + Pixel Piksel + Adjustment Ayarlama + Drag (lasso) or click points (polygonal) · {add} add · {sub} subtract Noktaları sürükleyin (kement) veya tıklayın (çokgen) · {add} ekle · {sub} çıkar + Drag a crop box · drag inside to move · edges resize ({ratio} ratio, {centre} centre) · Space moves while drawing · arrows nudge · X swaps orientation · {commit} commits · Esc cancels Kırpma kutusunu sürükleyin · taşımak için içeriye sürükleyin · kenarları yeniden boyutlandırın ({ratio} oranı, {centre} merkez) · Çizim sırasında boşluk hareket eder · oklar iter · X yönünü değiştirir · {commit} gerçekleştirir · Esc iptal eder + {mode} Color · {bits} bit {mode} Renkli · {bits} bit + Scrubby Zoom Süpürge Yakınlaştırma + Auto Erase Otomatik Silme + Color Adaptation Renk Uyarlaması + Preserve Transparency Şeffaflığı Koruyun + New Style… Yeni Stil… + shortcut kısayol + Saved Kaydedildi + Arc Yay + Arch Kemer + Bulge çıkıntı + Shell Lower Kabuk Alt Kısmı + Shell Upper Kabuk Üstü + Flag Bayrak + Fish Balık + Wave Dalga + Rise Yüksel + Inflate Şişir + Twist Büküm + Squeeze Sıkıştır + Mask Maske + Channel Kanal + Rectangle Dikdörtgen + New Channel Yeni Kanal + Save Selection Seçimi Kaydet + Load Selection Yük Seçimi +@id edit.purge.undo Geri Al + Rendering Mode İşleme Modu + Automatic (recommended) Otomatik (önerilen) + CPU / Compatibility CPU / Uyumluluk + Automatic uses GPU acceleration when available and falls back to CPU rendering on errors. Otomatik, mümkün olduğunda GPU hızlandırmayı kullanır ve hatalarda CPU oluşturmaya geri döner. + Switched to CPU rendering CPU oluşturmaya geçildi + GPU acceleration could not continue. Your documents are unchanged. GPU hızlandırması devam edemedi. Belgeleriniz değişmedi. + PhotoCraft is using the CPU image compositor. The window may still use your graphics adapter. PhotoCraft, CPU görüntü düzenleyicisini kullanıyor. Pencere hala grafik bağdaştırıcınızı kullanıyor olabilir. + Retry GPU requires a restart. Save your work first. GPU'yu yeniden dene, yeniden başlatma gerektirir. Öncelikle çalışmanızı kaydedin. + Keep Using CPU CPU'yu Kullanmaya Devam Edin + Retry GPU GPU'yu yeniden dene + Advanced Gelişmiş + GPU Backend GPU Arka Ucu + Details Ayrıntılar + Rendering mode Oluşturma modu + Freeze Lasso: drag to freeze an area, Alt-drag to thaw it (L) Kementi Dondur: Bir alanı dondurmak için sürükleyin, buzunu çözmek için Alt tuşuna basarak sürükleyin (L) + 3 x 3 3 x 3 + 4 x 4 4 x 4 + 5 x 5 5 x 5 + Camera Raw Filter ({layer}) Camera Raw Filtresi ({layer}) + Camera Raw ({name}) Camera Raw ({name}) + Edit Type Türü Düzenle + Grid: Izgara: + Highlight Clipping Warning (O) Kırpılma Uyarısını Vurgula (O) + Lasso around an area, then drag the selection Bir alanı kementle çevreleyin, ardından seçimi sürükleyin + Patch: Yama: + Shadow Clipping Warning (U) Gölge Kırpılması Uyarısı (U) + Show Lab Color Readouts Laboratuvar Renk Okumalarını Göster + Show Red at 3 o'clock Kırmızıyı saat 3 yönünde göster + Show Selected Region Seçilen Bölgeyi Göster + Show Skin Tone Indicator Cilt Tonu Göstergesini Göster + Show Vectorscope Vectorscope'u göster + Tone Histogram Ton Histogramı + 75% 75% + 80% 80% + 85% 85% + 90% 90% + 95% 95% + 125% 125% + 150% 150% + 175% 175% + 250% 250% + 300% 300% + No patterns Desen yok + Enable {name} {name}'yi etkinleştir + Hold Alt while drawing for straight segments Düz parçalar çizerken Alt tuşunu basılı tutun + Drag or click polygon points · hold Alt during lasso for straight segments · {add} add · {sub} before drawing subtracts Çokgen noktalarını sürükleyin veya tıklayın · düz parçalar için kement sırasında Alt tuşunu basılı tutun · {add} ekleyin · çizim çıkarmadan önce {sub} + Extend Uzat + Structure: Yapı: + Content-Aware Move Tool İçeriğe Uygun Taşıma Aracı + TIFF Options TIFF Seçenekleri + Targa Options Targa Seçenekleri + 24 bits/pixel 24 bit/piksel + 32 bits/pixel 32 bit/piksel + Including layers will increase file size. Katmanların eklenmesi dosya boyutunu artıracaktır. + Discard Layers and Save a Copy Katmanları Atın ve Bir Kopyasını Kaydedin + Don't show again Bir daha gösterme + Save failed: {error} Kaydetme başarısız oldu: {error} + Save cancelled Kaydetme iptal edildi + Style {name} saved {name} stili kaydedildi + Make Default Varsayılan Yap + Reset to Default Varsayılana Sıfırla + Couldn't place {name}: {error} {name} yerleştirilemedi: {error} + Full-resolution preview unavailable Tam çözünürlüklü önizleme kullanılamıyor + Magnetic Lasso Tool Manyetik Kement Aracı + Frequency Frekans + Use tablet pressure to change pen width Kalem genişliğini değiştirmek için tablet basıncını kullanın + About Hakkında + Contributors Katkıda Bulunanlar + Models Modeller + Sort Sırala + Username Kullanıcı adı + Display name Görünen ad + Real name Gerçek isim + Reverse the order Sırayı tersine çevir + Grab bag Çantayı al + Table Tablo + Name (A–Z) Ad (A–Z) + Merged PRs Birleştirilmiş PR'ler + Commits Taahhütler + Lines added Satırlar eklendi + Lines deleted Satırlar silindi + Line delta Çizgi deltası + Binary assets added İkili varlıklar eklendi + Binary assets removed İkili varlıklar kaldırıldı + First commit İlk taahhüt + First İlk + Last commit Son taahhüt + Last Son + Contributors: {contributors} · Commits: {commits} Katkıda Bulunanlar: {contributors} · Taahhütler: {commits} + No contributor data was built into this copy. Bu kopyaya hiçbir katkıda bulunan verisi eklenmedi. + No model credits were built into this copy. Bu kopyaya hiçbir model kredisi eklenmemiştir. + Company Şirket + Version Sürüm + % of all commits Tüm taahhütlerin yüzdesi + Lines +/− Çizgiler +/− + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR'ler, {commits} taahhütler, +{added} / −{deleted} satırlar (Δ {delta}), +{binAdded} / −{binDeleted} ikili varlıklar, {first} – {last} + Create new swatch Yeni renk örneği oluştur + New Swatch… Yeni Swatch… + New Swatch Group… Yeni Swatch Grubu… + Import Swatches… Renk Örneklerini İçe Aktar… + Replace Swatches… Renk Örneklerini Değiştirin… + Export Swatches… Renk Örneklerini Dışa Aktar… + Export Swatches for Exchange… Değişim için Renk Örneklerini Dışa Aktar… + Reset Swatches Renk Örneklerini Sıfırla + Add to Swatches Renk Örneklerine Ekle + Blend If: Eğer Karışım: + Channels: Kanallar: + Knockout Nakavt + Apply At Canvas Bounds Kanvas Sınırlarında Uygula + Color Picker (Text Color) Renk Seçici (Metin Rengi) + Color Picker ({name}) Renk Seçici ({name}) + Color Picker (Fill Color) Renk Seçici (Dolgu Rengi) + Fill color Rengi doldur + Color Picker (Stroke Color) Renk Seçici (Kontur Rengi) + Stroke color Kontur rengi + Pick screen color Ekran rengini seç + Sample a pixel anywhere on the screen Ekranın herhangi bir yerinde bir pikseli örnekleyin + Click to pick · Esc to cancel Seçmek için tıklayın · İptal etmek için Esc + Pick a screen pixel or press Esc to cancel Bir ekran pikseli seçin veya iptal etmek için Esc tuşuna basın + Scroll to zoom Yakınlaştırmak için kaydırın + Show/Hide All Panels Tüm Panelleri Göster/Gizle + Show/Hide Panels Panelleri Göster/Gizle + Could not open Açılamadı + {path}: the path is not valid Unicode; rename the file and open it again. {path}: yol geçerli bir Unicode değil; dosyayı yeniden adlandırın ve tekrar açın. + Contents: İçerik: + Custom Pattern: Özel Desen: + Foreground Color Ön Plan Rengi + Color… Renk… + 50% Gray %50 Gri + {size} (was {old}) {size} ({old} idi) + Cone Koni + Cone (Inverted) Koni (Ters Çevrilmiş) + Domed Kubbeli + Domed (Inverted) Kubbeli (Ters) + Diagonal (Descending) Çapraz (Azalan) + Contour Editor Kontur Düzenleyici + Anti-aliased Kenar yumuşatma + Corner Köşe + Gloss Contour Parlak Kontur + Contour Kontur + Cone - Inverted Koni - Ters + Cove - Deep Koy - Derin + Cove - Shallow Koy - Sığ + Half Round Yarım Yuvarlak + Ring - Double Yüzük - Çift Kişilik + Rolling Slope - Descending Yuvarlanan Eğim - Azalan + Rounded Steps Yuvarlak Adımlar + Sawtooth 1 Testere dişi 1 + Red Eye Tool Kırmızı Göz Aracı + Red Eye Kırmızı Göz + Pupil Size Öğrenci Boyutu + Darken Amount Koyulaştırma Miktarı + Click a red pupil to neutralize it Nötrleştirmek için kırmızı bir gözbebeğine tıklayın + open source açık kaynak + …and {n} more …ve {n} daha + Eyedroppers: Damlalıklar: + Set Black Point Siyah Noktayı Ayarla + Set Neutral Gray Point Nötr Gri Noktayı Ayarla + Set White Point Beyaz Noktayı Ayarla + Unavailable in this color mode Bu renk modunda kullanılamaz + Selection Preview: Seçim Önizlemesi: + Black Matte Siyah Mat + White Matte Beyaz Mat +layerLabel No Color Renk Yok + Seafoam Deniz köpüğü + Indigo Çivit mavisi + Fuchsia Fuşya + Violet Menekşe + Large document: redrawing on the CPU Büyük belge: CPU'da yeniden çizim + This redraw took {seconds} s on the CPU. Bu yeniden çizim CPU'da {seconds} saniye sürdü. + Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. Bir katmanın karışım modu, opaklığı veya görünürlüğü gibi belgenin tamamını değiştiren düzenlemeler, belgenin tamamını yeniden çizer. + Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. Katmanları GPU bellek bütçesine uymuyor. Tercihler'de Bellek kullanımını artırma › Performans, bütçeyi bu bilgisayarın belleğinin dörtte birine kadar artırır. + The GPU compositor wasn't used: {reason} GPU dizgicisi kullanılmadı: {reason} +cameraRaw Light Açık Tonlar +fontWeight Thin İnce +fontWeight ExtraLight Ekstra İnce +fontWeight Light Hafif +fontWeight Regular Normal +fontWeight Medium Orta +fontWeight SemiBold Yarı Kalın +fontWeight Bold Kalın +fontWeight ExtraBold Ekstra Kalın +fontWeight Black En Kalın +fontWeight Italic İtalik + Pattern Stamp Tool Desen Damgası Aracı + Pattern Stamp Desen Damgası + Impressionist Empresyonist + Rotate View Görünümü Döndür + Rotate View Tool Görünümü Döndürme Aracı + Reset View Görünümü Sıfırla + Drag around the centre to rotate the view · Shift constrains to 15° Görünümü döndürmek için merkezin etrafında sürükleyin · Kaydırma 15° ile sınırlandırılır +@id select.all Tümünü Seç +@id edit.stroke Kontur… +@id edit.transform.scale Ölçekle +cameraRaw Red Kırmızı +cameraRaw Orange Turuncu +cameraRaw Yellow Sarı +cameraRaw Green Yeşil +cameraRaw Aqua Camgöbeği +cameraRaw Blue Mavi +cameraRaw Purple Mor +cameraRaw Magenta Macenta +cameraRaw Blending Karışım +cameraRaw Luminance Parlaklık +cameraRaw Temperature Sıcaklık +cameraRaw Color Renk + Patch Tool Yama Aracı + Preferences… Tercihler… + Settings Ayarlar + Duplicate Çoğalt + Actual Pixels Gerçek Pikseller + Hide Gizle + New Layer… Yeni Katman… + Clipping Mask Kırpma Maskesi + Tool Options Araç Seçenekleri + Color Picker Renk Seçici + Color Swatches Renk Örnekleri + Linear Dodge Doğrusal Soldurma + Preview Image Resmin Önizlemesi + Automatic Otomatik + Save As Farklı Kaydet + Import… İçe aktar… + Remove All Tümünü Kaldır + Unlock Kilidi aç + Enable Etkinleştir + Disable Devre dışı bırak + On Açık + Selected Seçilen + Clear Selection Seçimi Temizle + Invert Selection Seçimi Ters Çevir + Select Inverse Tersini Seç + Contract Sözleşme + Sample Size Örnek Boyutu + Strength Güç + Gamma Gama + Input Giriş + Output Çıkış + Input Levels Giriş Seviyeleri + Output Levels Çıkış Seviyeleri + Layer Styles Katman Stilleri + Blending Mode Karıştırma Modu + Fill Color Dolgu Rengi + Stroke Color Kontur Rengi + Stroke Width Kontur Genişliği + Shadow Gölge + Adjustment Layer Ayarlama Katmanı + Adjustment Layers Ayarlama Katmanları + Text Layer Metin Katmanı + Documents Belgeler + File Name Dosya Adı + File Format Dosya Formatı + Compression Sıkıştırma + Artboards Çalışma yüzeyleri + Snap to Grid Izgaraya Yasla + Full Screen Tam Ekran + Fullscreen Tam ekran + Workspaces Çalışma alanları + Alignment Hizalama + Step Backward Geri Adım + Step Forward İleri Adım + Show All Tümünü Göster + Hidden Gizli + New Layer Yeni Katman + Duplicate Layer Katmanı Çoğalt + Ungroup Grubu çöz + Lock Transparent Pixels Şeffaf Pikselleri Kilitle + Lock Image Pixels Görüntü Piksellerini Kilitle + Lock Position Kilit Konumu + Lock All Tümünü Kilitle + Show Rulers Cetvelleri Göster + Show Grid Izgarayı Göster + Guide Layout… Kılavuz Düzeni… + Page Setup… Sayfa Yapısı… + Reset Preferences Tercihleri Sıfırla + Documentation Dokümantasyon + Check for Updates Güncellemeleri Kontrol Et + Loading… Yükleniyor… + Processing… İşleniyor… + Saving… Kaydediliyor… + Exporting… Dışa aktarılıyor… + Please wait… Lütfen bekleyin… + No document open Hiçbir belge açık değil + No selection Seçim yok + No layers Katman yok + No recent files Yeni dosya yok + Unsupported file format Desteklenmeyen dosya biçimi + Failed to open file Dosya açılamadı + Failed to save file Dosya kaydedilemedi + Operation canceled İşlem iptal edildi + Changes have not been saved. Değişiklikler kaydedilmedi. + Are you sure? Emin misin? + Discard Changes Değişiklikleri İptal Et + Recent Files Son Dosyalar + Open File Dosyayı Aç + Save File Dosyayı Kaydet + Save Changes Değişiklikleri Kaydet + Rendering… Oluşturuluyor… + Importing… İçe aktarılıyor… + Ready Hazır + Failed Başarısız + Warning Uyarı + Information Bilgi + Errors Hatalar + Retry Yeniden dene + Continue Devam et + Skip Atla + Stop Durdur + Resume Devam et + Reset All Tümünü Sıfırla + Defaults Varsayılanlar + Apply to All Tümüne Uygula + Confirm Onayla + Finish Bitiş + Back Geri + Next Sonraki + Previous Önceki + More Daha Fazla + Less Daha az + Manual Manuel + Disabled Devre dışı + Locked Kilitli + Unlocked Kilitli değil + Delete… Sil… + Create… Oluştur… + Edit… Düzenle… + Change… Değiştir… + Transformation Dönüşüm + Scaling Ölçeklendirme + Canvas Size Kanvas Boyutu + Percentage Yüzde + Degrees Dereceler + Middle Orta + Location Konum + Location: Konum: + Diameter Çap + Intensity Yoğunluk + Sensitivity Hassasiyet + Softness Yumuşaklık + Feather Radius Tüy Yarıçapı + Color Profile Renk Profili + Library Kütüphane + Libraries Kütüphaneler + Layer Properties Katman Özellikleri + Groups Gruplar + Masks Maskeler + Add Mask Maske Ekle + Delete Mask Maskeyi Sil + Invert Mask Maskeyi Ters Çevir + Enable Mask Maskeyi Etkinleştir + Disable Mask Maskeyi Devre Dışı Bırak + Apply Mask Maske Uygula + Import Settings Ayarları İçe Aktarma + Export Settings Ayarları Dışa Aktarma + Shortcut Kısayol + Shortcuts Kısayollar + Keyboard Klavye + Mouse Fare + Tablet tablet + Display Ekran + Panel Paneli + Hide Grid Izgarayı Gizle + Fit to Window Pencereye Sığdır + Dark Theme Koyu Tema + Light Theme Işık Teması + Live Preview Canlı Önizleme + Before önce + After Sonra + Compare Karşılaştır + File Size Dosya Boyutu + Filename Dosya adı + Folder Klasör + Open Folder Klasörü Aç + Choose Folder Klasör Seçin + Browse Göz at + Find Bul + Replace Değiştir + Category Kategori + Date Tarih + Time Zaman + Learn More Daha Fazla Bilgi Edinin + Report a Bug Hata Bildir + Updates Güncellemeler + Update Güncelleme + Restart Required Yeniden Başlatma Gerekli + Restart Yeniden başlat + Quit Çıkış + Quit PhotoCraft PhotoCraft'tan Çık + Close Window Pencereyi Kapat + New Window Yeni Pencere + Open Image Resmi Aç + Save Image Resmi Kaydet + Export Image Görüntüyü Dışa Aktar + Import Image Görüntüyü İçe Aktar + Export As Farklı Dışa Aktar + Export to Şuraya aktar: + Export Options Dışa Aktarma Seçenekleri + Import Options İçe Aktarma Seçenekleri + No Selection Seçim Yok + No Document Belge Yok + No Image Resim Yok + No Layers Katman Yok + No Results Sonuç Yok + No Results Found Sonuç Bulunamadı + Nothing to Undo Geri Alınacak Bir Şey Yok + Nothing to Redo Tekrarlanacak bir şey yok + Changes Saved Değişiklikler Kaydedildi + Unsaved Changes Kaydedilmemiş Değişiklikler + Save before closing? Kapatmadan önce kaydet? + Are you sure you want to quit? Vazgeçmek istediğinden emin misin? + The file could not be opened. Dosya açılamadı. + The file could not be saved. Dosya kaydedilemedi. + Unsupported format Desteklenmeyen biçim + Out of memory Bellek yetersiz + Permission denied İzin reddedildi + File not found Dosya bulunamadı + Operation failed İşlem başarısız oldu + Operation completed İşlem tamamlandı + Operation cancelled İşlem iptal edildi + Camera Calibration Kamera Kalibrasyonu + Snapshots Anlık görüntüler + Recovery Kurtarma + Fill Light Işık Doldur + Convert to Grayscale Gri Tonlamaya Dönüştür + Sharpness Keskinlik + Color Noise Reduction Renk Gürültü Azaltma + Luminance Smoothness Parlaklık Pürüzsüzlüğü + Focal Length35mm Odak Uzunluğu35mm + Aspect Ratio En Boy Oranı + Enter description… Açıklamayı girin… + Industry Standard Endüstri Standardı + Uncoated CMYK Kaplamasız CMYK + Working Spaces Çalışma Alanları + Color Management Policies Renk Yönetimi Politikaları + Detect Faces Yüzleri Algıla + Localize Color Clusters Renk Kümelerini Yerelleştirin + Duplicated selection name Yinelenen seçim adı + New channel only Yalnızca yeni kanal + Masked Areas Maskelenmiş Alanlar + Selected Areas Seçilen Alanlar + Color indicates Renk şunu gösterir: + Create Snapshot Anlık Görüntü Oluştur + Delete Snapshot Anlık Görüntüyü Sil + New Snapshot Yeni Anlık Görüntü + Snapshot 1 Anlık Görüntü 1 + Nonlinear History Doğrusal Olmayan Geçmiş + Set New Snapshot as Default Yeni Anlık Görüntüyü Varsayılan Olarak Ayarla + Automatically Create First Snapshot İlk Anlık Görüntüyü Otomatik Olarak Oluştur + Automatically Create New Snapshot When Saving Kaydederken Otomatik Olarak Yeni Anlık Görüntü Oluştur + Show New Snapshot Dialog by Default Yeni Anlık Görüntü İletişim Kutusunu Varsayılan Olarak Göster + Clear History Geçmişi Temizle + Panel Options… Panel Seçenekleri… + View menu Menüyü görüntüle + Keep view in sync Görünümü senkronize tutun + Mouse Controls Fare Kontrolleri + Initial Direction of Stroke Başlangıç Strok Yönü + Direction of Stroke İnme Yönü + Humidity Nem + Auto Clean Brush Otomatik Temizleme Fırçası + Brush Tip Fırça Ucu + New Layer from Brush Brush'tan Yeni Katman + New Brush Yeni Fırça + New Tool Preset Yeni Araç Ön Ayarı + Auto-Leading Otomatik Satır Aralığı + Scale Horizontally Yatay Olarak Ölçeklendir + Scale Vertically Dikey Ölçeklendir + Baseline Shift Taban Çizgisi Kayması + Superscript Üst simge + Subscript Abonelik + Ligature Ligatür + Alternative Alternatif + Extra Light Ekstra Işık + Extra Bold Ekstra Kalın + Sharpen Amount Keskinleştirme Tutarı + Sharpen Radius Yarıçapı Keskinleştir + Sharpen Detail Detayı Keskinleştir + Sharpen Masking Maskelemeyi Keskinleştir + Noise Luminance Gürültü Parlaklığı + Noise Luminance Detail Gürültü Parlaklığı Ayrıntısı + Noise Color Gürültü Rengi + Noise Color Detail Gürültü Rengi Ayrıntısı + Grain Amount Tane Miktarı + Grain Size Tane Boyutu + Grain Roughness Tahıl Pürüzlülüğü + Vignette Roundness Vinyet Yuvarlaklığı + Vignette Feather Vinyet Tüyü + Vignette Highlights Vinyet Öne Çıkanlar + Vignette Style Vinyet Stili + Planes Uçaklar + Corners Köşeler + Plane Uçak + Shadow Amount Gölge Miktarı + Shadow Tone Gölge Tonu + Shadow Radius Gölge Yarıçapı + Highlight Amount Tutarı Vurgula + Highlight Tone Vurgu Tonu + Highlight Radius Yarıçapı Vurgula + Midtone Orta ton + Black Clip Siyah Klip + White Clip Beyaz Klip + Fuzziness bulanıklık + Source Layer Kaynak Katmanı + Neutralize Nötrleştir + Use Selection In Source Kaynaktaki Seçimi Kullan + Use Selection In Target Hedefteki Seçimi Kullan + Flame Type Alev Tipi + Randomize Length Uzunluğu Rastgeleleştir + Interval Aralık + Adjust Interval For Loops Döngüler İçin Aralığı Ayarla + Use Custom Color Özel Renk Kullan + Flame Lines Alev Hatları + Flame Bottom Alignment Alev Alt Hizalaması + Flame Style Alev Stili + Flame Shape Alev Şekli + Randomize Shapes Şekilleri Rastgeleleştir + Frame Çerçeve + Margin Marj + Arrangement Düzenleme + Thickness Kalınlık + Vine Color Asma Rengi + Flower Color Çiçek Rengi + Leaf Color Yaprak Rengi + Base Tree Type Temel Ağaç Türü + Light Direction Işık Yönü + Leaves Amount Yaprak Tutarı + Leaves Size Yaprak Boyutu + Branches Height Dal Yüksekliği + Branches Thickness Dal Kalınlığı + Default Leaves Varsayılan İzinler + Leaves Color Yaprak Rengi + Custom Branch Color Özel Şube Rengi + Branches Color Dal Rengi + Flat Shading Düz Gölgeleme + Enable Timeline Quick Config Zaman Çizelgesi Hızlı Yapılandırmasını Etkinleştir + Set Start Timecode… Başlangıç Zaman Kodunu Ayarla… + Timecode Zaman kodu + Current Time Güncel Saat + Duration Süre + Repeat Tekrarla + Play Once Bir Kez Oyna + Info Options… Bilgi Seçenekleri… + First Color Readout İlk Renk Okuması + Second Color Readout İkinci Renk Okuması + Actual Color Gerçek Renk + Grayscale Slider Gri Tonlama Kaydırıcısı + Web Color Web Rengi + Total Ink Toplam Mürekkep + Opacity Mask Opaklık Maskesi + Document Size Belge Boyutu + Timing Zamanlama + Status Information Durum Bilgileri + Mouse Coordinates Fare Koordinatları + Ruler Units Cetvel Birimleri + Restore last viewed comp Son görüntülenen kompozisyonu geri yükle + Select All Layers Tüm Katmanları Seç + Keep proportions Oranları koruyun + Interpolation Enterpolasyon + Caption Başlık + Caption Credits Altyazı Kredisi + Keywords Anahtar Kelimeler + Copyright Info URL Telif Hakkı Bilgisi URL'si + Copyright Status Telif Hakkı Durumu + Copyrighted Telif hakkıyla korunuyor + Public Domain Kamu Alanı + Unknown Bilinmiyor + Credit Kredi + Transmission Reference İletim Referansı + Urgency Aciliyet + High Yüksek + Low Düşük + Supplemental Categories Tamamlayıcı Kategoriler + Object Name / Title Nesne Adı / Başlığı + Date Created Oluşturulma Tarihi + City Şehir + Province / State İl / Eyalet + Country Ülke + Byline Yazar yazısı + Byline Title Yazar Başlığı + Phone Telefon + Email E-posta + Last Modified Son Değiştirilme + Camera Data Kamera Verileri + Camera Model Kamera Modeli + Camera Make Kamera Markası + EXIF Version EXIF Sürümü + Exposure Program Maruziyet Programı + Aperture Diyafram + Shutter Speed Enstantane Hızı + Metering Mode Ölçüm Modu + Light Source Işık Kaynağı + Flash Flaş + Create Multiple Picture Packages Çoklu Resim Paketleri Oluşturun + Choose… Seç… + Select Folder… Klasör Seç… + Override Action "Save" Commands Eylem "Kaydet" Komutlarını Geçersiz Kıl + File Naming Dosya Adlandırma + Compatibility Uyumluluk + Include ICC Profile ICC Profilini Ekle + Error File Hata Dosyası + Stop For Errors Hatalar İçin Durdur + Log Errors To File Hataları Dosyaya Günlüğe Kaydet + Settings: Ayarlar: + Working Space Çalışma Alanı + Advanced Controls Gelişmiş Kontroller + Desaturate Monitor Colors By Monitör Renklerinin Doygunluğunu Azaltma + Blend RGB Colors Using Gamma 1.0 Gama 1.0 Kullanarak RGB Renklerini Karıştırın + Profile Mismatches: Ask When Opening Profil Uyuşmazlıkları: Açarken Sor + Ask When Pasting Yapıştırırken Sor + Missing Profiles: Ask When Opening Eksik Profiller: Açarken Sor + Embed profiles Profilleri yerleştir + Coerce to sRGB sRGB'ye baskı yapın + Graphic and web Grafik ve web + Minimalistic Minimalist + Expand All Groups Tüm Grupları Genişlet + Recent Items Son Öğeler + Delete Preset… Ön Ayarı Sil… + Preset Name: Ön Ayar Adı: + Width and Height Genişlik ve Yükseklik + Units Birimler + Columns Sütunlar + Create new count group Yeni sayım grubu oluştur + Drag to move the layer Katmanı taşımak için sürükleyin + Go to Layer Katmana Git + Current Mevcut + Reference point location Referans noktası konumu + Toggle reference point Referans noktasını değiştir + Heal Off İyileşme + Heal On İyileşme + Heal Auto Otomatik İyileştirme + Brush Size Fırça Boyutu + Heal Color Rengi İyileştir + Result Sonuç + Fast Hızlı + Accurate Doğru + Add to Ekle + Subtract from Şundan çıkar: + Intersect with İle kesiştir + Capture Comp Comp'ı Yakala + No Layer Comps Katman Kompozisyonu Yok + Add to selection (⇧) Seçime ekle (⇧) + Click to zoom in · ⌥-click to zoom out Yakınlaştırmak için tıklayın · Uzaklaştırmak için ⌥-tıklayın + Commit Perspective Warp (↩) Perspektif Çarpıtma İşlemi (↩) + Commit Puppet Warp (↩) Kukla Çarpıtma İşlemi (↩) + Commit any current edits (⌘↩) Mevcut düzenlemeleri taahhüt et (⌘↩) + Commit current crop operation (↵) Mevcut kırpma işlemini gerçekleştir (↵) + Commit transform (↩) Dönüşümü kaydet (↩) + Commit warp (↩) Çözgü gerçekleştir (↩) + Create a new layer (⇧⌘N) Yeni bir katman oluştur (⇧⌘N) + Custom pattern Özel desen + General Brushes Genel Fırçalar + New document… ⌘N Yeni belge… ⌘N + Open… ⌘O Aç… ⌘O + Search commands (⌘K) Arama komutları (⌘K) + Uses the foreground and background colors. Ön plan ve arka plan renklerini kullanır. + Collapse group (⌥-click: all groups) Grubu daralt (⌥ tuşuna basarak tıklayın: tüm gruplar) + Expand group (⌥-click: all groups) Grubu genişlet (⌥ tuşuna basarak tıklayın: tüm gruplar) + Date and Time Tarih ve Saat + Scale Units Ölçek Birimleri + Scale Factor Ölçek Faktörü + Area Alan + Perimeter Çevre + Circularity Dairesellik + Gray Value (Minimum) Gri Değeri (Minimum) + Gray Value (Maximum) Gri Değeri (Maksimum) + Gray Value (Mean) Gri Değeri (Ortalama) + Gray Value (Median) Gri Değeri (Medyan) + Integrated Density Entegre Yoğunluk + Length Uzunluk + In Black Siyah + In White Beyaz + Out Black Siyah + Out White Beyaz Dışı + Tint Color Ton Rengi + Stops Duraklar + Center X Merkez X + Center Y Merkez Y + Distribution Dağıtım + Monochromatic Tek renkli + Cell Size Hücre Boyutu + Generators Jeneratörler + Wavelength Min Dalga Boyu Min + Wavelength Max Maksimum Dalga Boyu + Amplitude Min Genlik Min + Amplitude Max Genlik Maksimum + Undefined Areas Tanımsız Alanlar + Preserve Koru + Max Radius Maksimum Yarıçap + Depth Mode Derinlik Modu + Solid Front Faces Katı Ön Yüzler + Mask Incomplete Blocks Tamamlanmamış Blokları Maskele + Stylization Stilizasyon + Cleanliness Temizlik + Bristle Detail Kıl Detayı + Lighting Aydınlatma + Shine Parıltı + Max Offset Maksimum Ofset + Map Document Harita Belgesi + Map Path Harita Yolu + Map Layer Harita Katmanı + Ridges Sırtlar + Light Type Işık Tipi + Light X Işık X + Light Y Işık Y + Light Z Açık Z + Target X Hedef X + Target Y Hedef Y + Hotspot Sıcak nokta + Elevation Yükseklik + Ambient Ortam + Warmth sıcaklık + Gloss parlaklık + Metallic metalik + Ambience ortam + White Is High Beyaz Yüksek + Reduce Color Noise Renk Gürültüsünü Azaltın + Sharpen Details Ayrıntıları Keskinleştir + Remove Jpeg Artifact Jpeg Yapısını Kaldır + Remove Tool Aracı Kaldır + Blade Curvature Bıçak Eğrisi + Depth Map Derinlik Haritası + Focal Distance Odak Mesafesi + Focus Odaklanma + Transition Geçiş + Radius X Yarıçap X + Radius Y Y Yarıçapı + Pins Pimler + Blur Angle Bulanıklık Açısı + Speed Hız + Taper Konik + Start X X'i başlat + Start Y Y'yi başlat + End X X'in sonu + End Y Y Sonu + Kernel çekirdek + Input Mode Giriş Modu + Row Order Satır Sırası + Eliminate Ortadan kaldır + Create By Oluşturan + List Liste + Pencil Width Kalem Genişliği + Stroke Pressure Strok Basıncı + Paper Brightness Kağıt Parlaklığı + Number Of Levels Seviye Sayısı + Edge Simplicity Kenar Sadeliği + Edge Fidelity Kenar Doğruluğu + Brush Detail Fırça Detayı + Highlight Area Alanı Vurgula + Glow Size Parıltı Boyutu + Glow Brightness Parıltı Parlaklığı + Brush Type Fırça Tipi + Stroke Size Kontur Boyutu + Stroke Detail Kontur Detayı + Highlight Strength Gücü Vurgula + Smoothness Pürüzsüzlük + Edge Thickness Kenar Kalınlığı + Edge Intensity Kenar Yoğunluğu + Posterization Posterleştirme + Stroke Length Strok Uzunluğu + Relief Rahatlama + Definition Tanım + Texture Coverage Doku Kapsamı + Shadow Intensity Gölge Yoğunluğu + Edge Width Kenar Genişliği + Edge Brightness Kenar Parlaklığı + Direction Balance Yön Dengesi + Black Intensity Siyah Yoğunluğu + White Intensity Beyaz Yoğunluğu + Dark Intensity Karanlık Yoğunluk + Custom Pixel Aspect Ratio Özel Piksel En Boy Oranı + 32-bit Preview Options 32 bit Önizleme Seçenekleri + New Guide Yeni Kılavuz + Join the ArtCraft Discord ArtCraft Discord'una katılın + Report an Issue Sorun Bildir + Load Files into Stack Dosyaları Yığına Yükle + Statistics İstatistikler + Duplicate Layers Katmanları Çoğalt + Edit Toolbar Araç Çubuğunu Düzenle + Import Brushes Fırçaları İçe Aktar + Load 3D LUT 3D LUT'u yükleyin + New document Yeni belge + Press keys Tuşlara basın + Rename Group Grubu Yeniden Adlandır + Rename Tool Preset Araç Ön Ayarını Yeniden Adlandır + Search commands, tools and panels Komutları, araçları ve panelleri arayın + Slice Options Dilim Seçenekleri + Rename Brush Fırçayı Yeniden Adlandır + Opening Açılış + Reading Okuma + Working Çalışma + Accented Edges Vurgulu Kenarlar + Angled Strokes Açılı Vuruşlar + Anisotropic Anizotropik + Around Center Merkez Çevresi + Artistic Sanatsal + Bamboo Bambu + Bas Relief Bas Rölyef + Berries Meyveler + Blast Patlama + Bleach Bypass Ağartma Baypası + Blocks Bloklar + Brick Tuğla + Brush Strokes Fırça Darbeleri + Burlap Çuval bezi + Candle Light Mum Işığı + Circle daire + Clumped Topaklanmış + Coarse Dots Kaba Noktalar + Colored Pencil Renkli Kalem + Contrasty Kontrastlı + Cool Harika + Craquelure Küçük çatlak + Cross Çapraz + Cross Process Çapraz Süreç + Crosshatch Çapraz tarama + Cutout Kesim + Daisies Papatyalar + Dark Rough Karanlık Sert + Dark Strokes Karanlık Vuruşlar + Darken Only Yalnızca Koyulaştır + Day For Night Gündüz Gece + Deep Blue Derin Mavi + Deep Emerald Derin Zümrüt + Deep Red Koyu Kırmızı + Deep Yellow Koyu Sarı + Dot Nokta + Double Line Çift Hat + Draft Taslak + Dry Brush Kuru Fırça + Duplication Çoğaltma + Edge Extension Kenar Uzantısı + Edge Only Yalnızca Kenar + Enlarged Büyütülmüş + Even Fields Çift Alanlar + Faded Film Solmuş Film + Film Grain Film Tahıl + Fine iyi + Fine Dots İnce Noktalar + Flat Düz + Fresco Fresk + From Left Soldan + From Right Sağdan + Frosted Buzlu + Generic jenerik + Glass Cam + Glowing Edges Parlayan Kenarlar + Grainy Dots Grenli Noktalar + Hearts Kalpler + Heptagon Yedigen + Hexagon Altıgen + Horizontal Only Yalnızca Yatay + Infinite Sonsuz + Ink Outlines Mürekkep Ana Hatları + Ivy sarmaşık + Left Diagonal Sol Çapraz + Level Based Seviyeye Dayalı + Light Rough Hafif Kaba + Lighten Only Yalnızca Açıklaştır + Long Lines Uzun Çizgiler + Long Strokes Uzun Vuruşlar + Lower Daha düşük + Medium Dots Orta Noktalar + Medium Lines Orta Çizgiler + Medium Strokes Orta Vuruşlar + Mono Contrast Mono Kontrast + Mosaic Tiles Mozaik Fayans + Multiple Flames Along Path Yol Boyunca Çoklu Alevler + Multiple Flames One Direction Çoklu Alevler Tek Yön + Multiple Flames Path Directions Çoklu Alev Yolu Yönleri + Multiple Flames Various Length Çoklu Alevler Çeşitli Uzunluklar + Octagon sekizgen + Odd Fields Tek Alanlar + One Flame Along Path Yol Boyunca Bir Alev + Out From Center Merkezden Dışarı + Oval oval + Overlay Edge Kaplama Kenarı + Palette Knife Palet Bıçağı + Parallel paralel + Patchwork yama işi + Pentagon Pentagon + Point Nokta + Pointed Sivri + Pond Ripples Gölet Dalgaları + Pyramids Piramitler + Random Rastgele + Right Diagonal Sağ Diyagonal + Ring Yüzük + Rope Halat + Roses Güller + Sandstone Kumtaşı + Scallops tarak + Sepia Sepya + Short Lines Kısa Çizgiler + Short Strokes Kısa Vuruşlar + Simple Basit + Light Intensity Işık Yoğunluğu + Spray Radius Püskürtme Yarıçapı + Stroke Direction Strok Yönü + Graininess Taneciklilik + Glow Amount Işıma Miktarı + Clear Amount Tutarı Temizle + Distortion Bozulma + Ripple Size Dalgalanma Boyutu + Ripple Magnitude Dalgalanma Büyüklüğü + Charcoal Area Kömür Alanı + Chalk Area Tebeşir Alanı + Charcoal Thickness Kömür Kalınlığı + Light Dark Balance Açık Karanlık Dengesi + Foreground Level Ön Plan Seviyesi + Background Level Arka Plan Düzeyi + Pattern Type Desen Türü + Image Balance Görüntü Dengesi + Darkness Karanlık + Fiber Length Elyaf Uzunluğu + Crack Spacing Çatlak Aralığı + Crack Depth Çatlak Derinliği + Crack Brightness Çatlak Parlaklığı + Grain Type Tahıl Türü + Tile Size Döşeme Boyutu + Grout Width Derz Genişliği + Lighten Grout Harcı hafifletmek + Square Size Kare Boyutu + Border Thickness Kenar Kalınlığı + Correct Distortion Bozulmanın Düzeltilmesi + Correct Vignette Doğru Vinyet + Correct C A Doğru CA + Auto Scale Otomatik Ölçeklendirme + Red Cyan Kırmızı Camgöbeği + Blue Yellow Mavi Sarı + Vignette Amount Vinyet Tutarı + Vignette Midpoint Vinyet Orta Noktası + Constraints Kısıtlamalar + Curve Highlights Eğrinin Öne Çıkan Noktaları + Curve Lights Eğri Işıkları + Curve Darks Eğri Koyuları + Curve Shadows Eğri Gölgeleri + Curve Splits Eğri Bölmeleri + Point Curve Nokta Eğrisi + Point Curve Red Nokta Eğrisi Kırmızı + Point Curve Green Nokta Eğrisi Yeşil + Point Curve Blue Nokta Eğrisi Mavi + Hsl Hue Hsl Ton + Hsl Sat HSL Cumartesi + Hsl Lum Hsl Lum + Grade Shadows Sınıf Gölgeleri + Grade Midtones Derece Orta Tonları + Grade Highlights Notun Öne Çıkan Noktaları + Grade Global Küresel Sınıf + Grade Blending Sınıf Harmanlama + Grade Balance Not Dengesi + Sine sinüs + Sketch Eskiz + Snowflakes Kar taneleri + Sparkle ışıltı + Spatter Sıçrama + Speckle Benek + Spin Döndürme + Sprinkles serpintiler + Squareness Karelik + Stagger Sendeleme + Stained Glass Vitray + Star Yıldız + Stars Yıldızlar + Stippled noktalanmış + Stretch Streç + Teal Orange turkuaz turuncu + Tiny Lens Minik Mercek + To Center Merkeze + Triangle Üçgen + Unaltered değiştirilmemiş + Underpainting Alt boyama + Underwater Sualtı + Uniform Üniforma + Upper Üst + Vertical Only Yalnızca Dikey + Vine With Flowers Çiçekli Asma + Vine With Leaves Yapraklı Asma + Violent Şiddet içeren + Warm Sıcak + Watercolor Suluboya + Waves Dalgalar + Wide Blurry Geniş Bulanık + Wide Sharp Geniş Keskin + Wrap Sarma + Zigzag Zikzak + Wetness ıslaklık + Delete Smart Filters Akıllı Filtreleri Sil + Enable Smart Filters Akıllı Filtreleri Etkinleştir + Delete Layer… Katmanı Sil… + Select Similar Colors Benzer Renkleri Seçin + New Selection Yeni Seçim + Click the first point or press ↵ to close · Esc cancels Kapatmak için ilk noktayı tıklayın veya ↵ tuşuna basın · Esc iptal eder + Click to add a point · drag off or ⌘/Ctrl-click or Delete to remove Nokta eklemek için tıklayın · sürükleyin veya kaldırmak için ⌘/Ctrl tuşuna basarak tıklayın veya Sil + Click to apply, ⌥-click to clear overrides, double-click for Style Options Uygulamak için tıklayın, geçersiz kılmaları temizlemek için ⌥-tıklayın, Stil Seçenekleri için çift tıklayın + Click: corner · Drag: smooth · Click first point: close · ↩ finish · Esc cancel Tıklayın: köşe · Sürükleyin: pürüzsüz · İlk noktaya tıklayın: kapat · ↩ bitir · Esc iptal + Rulers (⌘R) Cetveller (⌘R) + Show Guides (⌘;) Kılavuzları Göster (⌘;) + Grid (⌘') Izgara (⌘') + PDF Presentation… PDF Sunumu… + Picture Package… Resim Paketi… + Web Photo Gallery… Web Fotoğraf Galerisi… + Gray: Gri: + Spot: Nokta: + Technology Previews… Teknoloji Önizlemeleri… + 3D… 3 boyutlu… + AI Integrations Yapay Zeka Entegrasyonları + Add Noise Gürültü Ekle + Add Pins Pin Ekle + Add a mask (from the selection; ⌥ inverts) Bir maske ekleyin (seçimden; ⌥ tersine çevirir) + Adds grain to the soft edges of the brush tip. Fırça ucunun yumuşak kenarlarına gren ekler. + Adobe PDF Presets… Adobe PDF Hazır Ayarları… + Advanced Blending Gelişmiş Harmanlama + After (X) (X)'ten sonra + Airbrush: keeps painting while the pointer rests. Airbrush: İşaretçi dururken boyamaya devam eder. + Alternates for Current Selection Mevcut Seçimin Alternatifleri + Apply Data Set Veri Kümesini Uygula + Apply Image Resmi Uygula + Arbitrary keyfi + Artboard from Group Gruptan çalışma yüzeyi + Artboard from Layers Katmanlardan Çalışma Yüzeyi + Artboards to Files Çalışma Yüzeylerinden Dosyalara + Artboards to PDF Çalışma yüzeylerini PDF'ye dönüştürme + Ask Before Saving Saved Embedded Kaydedilen Gömülü Kaydetmeden Önce Sor + Assign Profile Profil Ata + At Şu tarihte: + Audio Ses + Auto-Align Layers Katmanları Otomatik Hizala + Auto-Blend Layers Katmanları Otomatik Karıştır + Auto-Hide Otomatik Gizle + Auto-leading % Otomatik satır aralığı % + Avoid Widows/Orphans Dul/Yetimlerden Kaçının + Background from Layer Katmandan Arka Plan + Batch Toplu + Blend Karışım + Border Sınır + Box Blur Kutu Bulanıklığı + Calculations Hesaplamalar + Camera Raw Camera Raw + Camera Raw Filter Camera Raw Filtresi + Canvas rotation Kanvas döndürme + Channel1 Kanal1 + Channel2 Kanal2 + Channel3 Kanal3 + Channel4 Kanal4 + Clipping Path Kırpma Yolu + Clone Klon + Color Decontaminate Renkli Dekontaminasyon + Color Halftone Renk Noktalı Resim + Color Lookup Tables Renk Arama Tabloları + Color Settings Renk Ayarları + Color Table Renk Tablosu + Compose Oluştur + Condensed Yoğunlaştırılmış + Conditional Mode Change Koşullu Mod Değişikliği + Content-Aware Fill İçeriğe Uygun Dolgu + Convert Frames > Video Layers Çerçeveleri Dönüştür > Video Katmanları + Convert to Linked Bağlantılıya Dönüştür + Convert to Profile Profile Dönüştür + Create Date Tarih Oluştur + Create Plane Düzlem Oluştur + Create new layer Yeni katman oluştur + Create new set Yeni grup oluştur + Crystallize Kristalleş + Data Veri + Data Sets as Files Dosya Olarak Veri Kümeleri + DateTime Original TarihSaat Orijinal + De-Interlace Taramasızlaştırma + Defaults restored. Varsayılanlar geri yüklendi. + Define Brush Preset Fırça Ön Ayarını Tanımla + Define Custom Shape Özel Şekli Tanımla + Define Pattern Deseni Tanımla + Defringe Bordürleri kaldır + Delete Pins Pinleri Sil + Diffuse Yaygın + Diffuse Glow Yaygın Parıltı + Digits Rakamlar + Displace Yer değiştir + Do you want to save you made to “{name}” before quitting? Çıkmadan önce yaptığınız işi “{name}” konumuna kaydetmek istiyor musunuz? + Dodge/Burn Soldurma/Yakma + Duplicate Path Yinelenen Yol + Dust & Scratches Toz ve Çizikler + Dynamics Dinamik + Edit Plane Düzlemi Düzenle + Export Contents İçeriği Dışa Aktar + Export Preferences Dışa Aktarma Tercihleri + Export/Import Presets Ön Ayarları Dışa Aktarma/İçe Aktarma + Extensions Uzantılar + Extrude Ekstrüzyon + Fibers Lifler + Field Blur Alan Bulanıklığı + File Info Dosya Bilgileri + Filter Gallery Filtre Galerisi + Find and Replace Text Metni Bul ve Değiştir + First Line Indent İlk Satır Girintisi + Fit Image Resmi Sığdır + Flame Alev + Flatten Frames Into Clips Çerçeveleri Klipsler halinde Düzleştirin + Flatten Frames Into Layers Çerçeveleri Katmanlara Düzleştirin + Focus Area Odak Alanı + Follows only edges this close to the pointer ([ and ] change it) Yalnızca işaretçiye bu kadar yakın olan kenarları takip eder ([ ve ] bunu değiştirir) + Frame Duration Kare Süresi + Frame Sync Çerçeve Senkronizasyonu + Gaussian Gaussian + Gaussian Blur Gauss Bulanıklığı + General Blend Genel Karışım + Global Light Küresel Işık + Glyph Glif + Glyph Scaling Glif Ölçekleme + Group from Layers Katmanlardan Gruplandır + Group · {n} layer Grup · {n} katman + HDR Toning HDR Tonlama + HSB Color HSB Rengi + HSL/Grayscale HSL/Gri tonlamalı + Hanging Punctuation Asılı Noktalama İşaretleri + Heal İyileşmek + Healing Şifa + Hide the layer's effects (⌥-click: all layers) Katmanın efektlerini gizleyin (⌥ tuşuna basarak tıklayın: tüm katmanlar) + High Pass Yüksek Geçiş + Higher values follow only edges that contrast sharply with their surroundings Daha yüksek değerler yalnızca çevreleriyle keskin bir kontrast oluşturan kenarları takip eder + Highlight Vurgula + How often fastening points are placed by themselves Sabitleme noktalarının kendi başlarına ne sıklıkla yerleştirildiği + ISO Speed ISO Hızı + Image Processor Görüntü İşlemcisi + In içinde + Install Plug-in Eklentiyi Yükle + Interpret Footage Görüntüyü Yorumlama + Intersect with selection (⇧⌥) Seçimle kesiştir (⇧⌥) + Iris Blur İris Bulanıklığı + Jag Jag + Justify Gerekçelendir + Keeps the current texture when switching presets. Ön ayarları değiştirirken mevcut dokuyu korur. + Label Etiket + Layer Comps to Files Dosyalara Katman Kompozisyonları + Layer Content Options Katman İçeriği Seçenekleri + Layer From Background Arka Plandan Katman + Layer from Background Arka Plandan Katman + Layers to Files Dosyalara Katmanlar + Legacy Eski + Lens Objektif + Lens Blur Mercek Bulanıklığı + Lens Correction Mercek Düzeltme + Lens Corrections Mercek Düzeltmeleri + Lens Flare Mercek Parlaması + Lens Info Objektif Bilgisi + Letter Spacing Harf Aralığı + Lighting Effects Aydınlatma Efektleri + Limit Playback Oynatmayı Sınırla + Liquify Sıvılaştır + Lock Layers Katmanları Kilitle + Loop Döngü + Lum Lum + Match Color Rengi Eşleştir + Measure Ölçü + Merge to HDR Pro HDR Pro'ya Birleştir + Mezzotint Mezotint + Modify Date Tarihi Değiştir + Mosaic Mozaik + Motion Blur Hareket Bulanıklığı + Native file drag-and-drop is not supported on Wayland yet. Use File › Open, or run PhotoCraft under XWayland with `WAYLAND_DISPLAY= photocraft`. Yerel dosya sürükle ve bırak özelliği henüz Wayland'de desteklenmiyor. Dosya › Aç'ı kullanın veya PhotoCraft'ı XWayland altında `WAYLAND_DISPLAY= photocraft` ile çalıştırın. + Neon Glow Neon Parıltı + New Guide Layout Yeni Kılavuz Düzeni + New Path Yeni Yol + New Video Group Yeni Video Grubu + New Video Layer Yeni Video Katmanı + New Video Layer from File Dosyadan Yeni Video Katmanı + Newspaper Gazete + Ocean Ripple Okyanus Dalgalanması + Oil Paint Yağlı Boya + Open As Farklı Aç + Operation Operasyon + Out Dışarı + Package Paket + Paint Daubs Boya Lekeleri + Path Blur Yol Bulanıklığı + Path to Illustrator… İllüstratöre Giden Yol… + Path: Yol: + Paths to Illustrator Illustrator'a Giden Yollar + Photomerge Fotoğraf Birleştir + Picture Frame Resim Çerçevesi + Pinch Tutam + Place Embedded Gömülü Yer + Place Linked Bağlantılı Yer + Place Scale Marker Ölçek İşaretçisini Yerleştirin + Plastic Wrap Plastik Sarma + Pointillize Noktalaştır + Polar Coordinates Kutupsal Koordinatlar + Polar To Rectangular Polar'dan Dikdörtgen'e + Poster Edges Poster Kenarları + Preset Manager Ön Ayar Yöneticisi + Profile Profil + Quick Sync Hızlı Senkronizasyon + Radial Blur Radyal Bulanıklık + Raw Data Ham Veri + Raw Defaults Ham Varsayılanlar + Rectangular To Polar Dikdörtgenden Polar'a + Reduce Noise Gürültüyü Azaltın + Relink to File Dosyaya Yeniden Bağlan + Render Video Videoyu Oluştur + Replace Color Rengi Değiştir + Replace Contents İçeriği Değiştir + Replace Footage Görüntüyü Değiştir + Resolve Missing Fonts Eksik Yazı Tiplerini Çözün + Ripple Dalgalanma + Roman Hanging Punctuation Roma Asılı Noktalama İşaretleri + Rotate Auto Otomatik Döndür + Rotate Fixed Sabit Döndür + Rough Pastels Kaba Pastel + Sat Cumartesi + Save a Copy Bir Kopyasını Kaydet + Save for Web (Legacy) Web için Kaydet (Eski) + Saved Multichannel Çok Kanallı Kaydedildi + Scale Effects Ölçek Efektleri + Script Events Manager Komut Dosyası Olay Yöneticisi + Search menus Arama menüleri + Seed tohum + Select Plane Düzlem Seç + Select and Mask Seç ve Maskele + Semi Bold Yarı Kalın + Set Measurement Scale Ölçüm Ölçeğini Ayarla + Shadows/Highlights Gölgeler/Vurgular + Shape Blur Şekil Bulanıklığı + Shear Kesme + Show the layer's effects (⌥-click: all layers) Katmanın efektlerini göster (⌥ tuşuna basarak tıklayın: tüm katmanlar) + Single Word Justification Tek Kelime Gerekçesi + Smart Blur Akıllı Bulanıklaştırma + Smart Sharpen Akıllı Keskinleştirme + Smudge Stick Leke Çubuğu + Solid Color Düz Renk + Spacing: Aralık: + Spherize Küreselleştir + Spin Blur Döndürme Bulanıklığı + Split Toning Bölünmüş Tonlama + Split at Playhead Oynatma Kafasında Böl + Spot nokta + Spot Channel Spot Kanal + Sprayed Strokes Püskürtülmüş Vuruşlar + Strokes Vuruşlar + Subject Distance Konu Mesafesi + Subtract from selection (⌥) Seçimden çıkar (⌥) + Sumi-e Sumi-e + Surface Blur Yüzey Bulanıklığı + Technology Previews Teknoloji Önizlemeleri + Texturizer Dokulaştırıcı + Tiles Fayans + Tilt Eğim + Tilt-Shift Tilt-Shift + Tone Curve Ton Eğrisi + Tool Araç + Toolbars Araç Çubukları + Trace Contour Konturu İzle + Trap Tuzak + Tree Ağaç + Trim Kırpma + Turbulent Çalkantılı + Twirl Burgu + Ultra Bold Ultra Kalın + Ultra Light Ultra Hafif + Unsharp Mask Keskinliği Azaltma Maskesi + Use File › Open to open an image. Bir resmi açmak için Dosya › Aç'ı kullanın. + Vanishing Point Ufuk Noktası + Variable Data Sets Değişken Veri Setleri + Video Frames to Layers Video Karelerinden Katmanlara + WIA Support WIA Desteği + Warp Text Metni Çözgü + Watercolour look: lighter interior, darker rims. Suluboya görünümü: daha açık iç kısım, daha koyu jantlar. + Wind Rüzgâr + Word Spacing Kelime Aralığı + Working Gray Plate Gri Çalışma Plakası + ZigZag ZigZag + ↑↓ navigate ↵ run esc close ↑↓ gezin ↵ çalıştır esc kapat +cameraRaw Amount Miktar +cameraRaw Blacks Siyahlar +cameraRaw Color Detail Renk Detayı +cameraRaw Color Grading Renk Derecelendirme +cameraRaw Darks Koyular +cameraRaw Global Genel +cameraRaw Hue Ton +cameraRaw Lights Açıklar +cameraRaw Luminance Detail Parlaklık Detayı +cameraRaw Masking Maskeleme +cameraRaw Roughness Pürüzlülük +cameraRaw Tint Renk Tonu +cameraRaw Vibrance Titreşim +cameraRaw Whites Beyazlar + Wand Değnek + Note tool Not aracı + Magnetic Lasso Manyetik Kement + Art History Brush Sanat Tarihi Fırçası + Spot Healing Nokta Şifası + Content-Aware Move İçeriğe Uygun Taşıma + Vertical Type Tool Dikey Metin Aracı + Click the first point or press {key} to close · {del} removes a point · Esc cancels İlk noktayı tıklayın veya kapatmak için {key} tuşuna basın · {del} bir noktayı kaldırır · Esc iptal eder + Click, then move along an edge · click to fasten a point · {alt}-click for a straight segment · double-click to close Tıklayın, ardından bir kenar boyunca hareket edin · bir noktayı sabitlemek için tıklayın · düz bir parça için {alt}-tıklayın · kapatmak için çift tıklayın + About {app} {app} Hakkında + Hide {app} {app}'ı gizle + Quit {app} {app}'dan çık + Services Hizmetler + Hide Others Diğerlerini Gizle + 3D 3D + Pen Kalem + Hand El + Sampler Örnekleyici + Patch Yama + Perspective Crop Perspektif Kırpma + RGB RGB + ISO ISO + ColorSync Renk Senkronizasyonu + ProPhoto ProFotoğraf + sRGB sRGB + ACES ACES + {zoom}% %{zoom} + Mac LCD Mac LCD'si + Mac Sharp Mac Keskin + Mac Crisp Mac Çıtır + Mac Strong Mac Güçlü + Mac Smooth Mac Pürüzsüz + Adobe Single-line Composer Adobe Tek Satır Oluşturucu + Adobe Every-line Composer Adobe Her Satır Oluşturucusu + X X + Y Y + 30 fps 30 fps + 25 fps 25 fps + 24 fps 24 fps + 15 fps 15 fps + 10 fps 10 fps + URLs URLs + EXIF EXIF + GPS GPS + Mac OS Mac OS + Unix Unix + RGB: RGB: + CMYK: CMYK: + sRGB IEC61966-2.1 sRGB IEC61966-2.1 + Adobe RGB (1998) Adobe RGB (1998) + ProPhoto RGB ProPhoto RGB + ColorMatch RGB ColorMatch RGB + Wide Gamut RGB Wide Gamut RGB + Apple RGB Apple RGB + NTSC (1953) NTSC (1953) + PAL/SECAM PAL/SECAM + SMPTE C SMPTE-C + HDTV (Rec. 709) HDTV (Kayıt 709) + CIE RGB CIE RGB + Photo (2 × 3 in) Fotoğraf (2 × 3 inç) + Photo (4 × 6 in) Fotoğraf (4 × 6 inç) + Photo (5 × 7 in) Fotoğraf (5 × 7 inç) + Photo (8 × 10 in) Fotoğraf (8 × 10 inç) + Lut Lut + Cooling L B B Soğutma L B B + Cooling80 Soğutma80 + Cooling82 Soğutma82 + Hsb HSB + Hsl HSL + Movie Prime Film Başbakanı + Prime105 Prime105 + Prime35 Prime35 + Rgb RGB + Sponge Sünger + Warming L B A Isınma L B A + Warming81 Isınma81 + Warming85 Isınma85 +@id filter.sharpen.sharpen Keskinleştir + Good iyi + Best En iyi + Out of gamut for printing: click for the nearest printable colour Yazdırma için gamın dışında: yazdırılabilir en yakın renk için tıklayın + Not a web colour: click for the nearest web colour Web rengi değil: en yakın web rengi için tıklayın + Adjustment Properties Ayarlama Özellikleri + Brush Stroke Fırça Darbesi + Default Foreground and Background Colors Varsayılan Ön Plan ve Arka Plan Renkleri + Move Layer Katmanı Taşı + Rectangular Selection Dikdörtgen Seçimi + Reorder Layer Katmanı Yeniden Sırala + Select Layer Katman Seç + Set Colors Renkleri Ayarla + Switch Foreground and Background Colors Ön Plan ve Arka Plan Renklerini Değiştir + Target Hedef + 3 by 3 Average 3'e 3 Ortalama + 5 by 5 Average 5'e 5 Ortalama + 11 by 11 Average 11'e 11 Ortalama + 31 by 31 Average 31'e 31 Ortalama + 51 by 51 Average 51'e 51 Ortalama + 101 by 101 Average 101'e 101 Ortalama + All Layers No Adjustments Tüm Katmanlar Ayarlama Yok + Color to Alpha… Alfa'ya Renk… + Current & Below No Adjustments Mevcut ve Altında Ayarlama Yok + Now drag the selection onto another area Şimdi seçimi başka bir alana sürükleyin + Point Sample Nokta Örneği + Sample Size: Örnek Boyutu: + Show Sampling Ring Örnekleme Halkasını Göster + Dual Axis Çift Eksen + Diagonal diyagonal + Transform Symmetry Simetriyi Dönüştür + Rasterize svg groups deeper than Svg gruplarını şundan daha derin bir şekilde rasterleştirin: + 0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. 0 bu eşiği kapatır; 100 düzeyden daha derine yerleştirilmiş SVG grupları her zaman rasterleştirilir. + Rule of Thirds Üçte Bir Kuralı + Golden Ratio Altın Oran + Golden Spiral Altın Spiral + Auto Show Overlay Otomatik Göster Yer Paylaşımı + Always Show Overlay Kaplamayı Her Zaman Göster + Never Show Overlay Kaplamayı Hiçbir Zaman Gösterme + Cycle Overlay Döngü Yerleşimi + Cycle Orientation Döngü Yönü + Set the overlay options for the Crop tool Kırpma aracının kaplama seçeneklerini ayarlama + Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Alt tuşuna basıp masaüstüne giderek pencereleri Alt tuşuyla sürükleyerek hareket ettirin. Pencere taşıma tuşunu Süper olarak ayarlayın veya Pencere › Değiştirici Tuşlar'da Alt'ı mandallayın. + Color Picker (Layer Style Color) Renk Seçici (Katman Stili Rengi) + Auto Hide Notifications Bildirimleri Otomatik Gizle + Notification Duration (seconds) Bildirim Süresi (saniye) + Appearance Mode Görünüm Modu + Next Appearance Mode Sonraki Görünüm Modu + Sync with system Sistemle senkronizasyon + Active Aktif + Color Picker (Solid Color) Renk Seçici (Düz Renk) + Straighten the image by drawing a line on it Üzerine bir çizgi çizerek görüntüyü düzeltin + Set additional Crop options Ek Kırpma seçeneklerini ayarlayın + Show Cropped Area Kırpılmış Alanı Göster + Enable Crop Shield Kırpma Kalkanını Etkinleştir + Match Canvas Maç Kanvası + Auto Adjust Opacity Opaklığı Otomatik Ayarla + Crop Shield color Kırpma Kalkanı rengi + Color Picker (Crop Shield Color) Renk Seçici (Kırpma Kalkanı Rengi) + From Center Merkezden + Link radii Bağlantı yarıçapları + Star Ratio: Yıldız Oranı: + Create Rectangle Dikdörtgen Oluştur + Create Ellipse Elips Oluştur + Create Triangle Üçgen Oluştur + Create Polygon Çokgen Oluştur + Pressure curve Basınç eğrisi + Click to add a point, right-click to remove Nokta eklemek için tıklayın, kaldırmak için sağ tıklayın + Some brush presets could not be loaded Bazı fırça ön ayarları yüklenemedi + Retry saving presets Ön ayarları kaydetmeyi yeniden deneyin + Brush preset changes are not saved: {error} Fırça hazır ayarı değişiklikleri kaydedilmiyor: {error} + Keep this tab open to retain your brushes. Fırçalarınızı korumak için bu sekmeyi açık tutun. + Browser storage is unavailable; brushes are session-only. Keep this tab open to retain them. Tarayıcı depolama alanı kullanılamıyor; fırçalar yalnızca oturum içindir. Bunları korumak için bu sekmeyi açık tutun. + Saving brush presets… Keep this tab open until saving finishes. Fırça ön ayarları kaydediliyor… Kaydetme bitene kadar bu sekmeyi açık tutun. + Reflect Yansıt + Glow Parıltı + Negation Olumsuzluk + XOR XOR + Color Burn (Paint.NET) Renk Yanması (Paint.NET) + Color Dodge (Paint.NET) Renk Soldurma (Paint.NET) + Pro Medium Profesyonel Orta + Studio Light Stüdyo Işığı + Stroke Options Kontur Seçenekleri + Solid Katı + Dashed Kesikli + Dotted Noktalı + Caps Kapaklar + Butt / Flat Popo / Düz + Miter Gönye + Round Yuvarlak + Bevel Eğim + Miter limit Gönye sınırı + Dashed line Kesikli çizgi + Dash Çizgi + Gap Boşluk + Dash / gap lengths × stroke width Çizgi / boşluk uzunlukları × kontur genişliği + Add pair Çift ekle + Remove pair Çifti kaldır + Dash offset Çizgi ofseti + Open paths use centered strokes Açık yollar ortalanmış konturlar kullanır + Use finite lengths and a positive pattern total Sonlu uzunluklar ve pozitif bir model toplamı kullanın + No subject found — try Object Selection or the Lasso Konu bulunamadı — Nesne Seçimi'ni veya Kement'i deneyin + Quick Selection found nothing there Hızlı Seçim orada hiçbir şey bulamadı + Object Selection found nothing there — try a tighter rectangle Nesne Seçimi burada hiçbir şey bulamadı; daha dar bir dikdörtgen deneyin + No subject found — try a rectangle around the object Konu bulunamadı — nesnenin etrafında bir dikdörtgen oluşturmayı deneyin + Appearance Settings… Görünüm Ayarları… + Add to favorites Favorilere ekle + Remove from favorites Favorilerden kaldır +cameraData Make Üretici +cameraData Model Model +cameraData Date Taken Çekim Tarihi +cameraData Exposure Pozlama +cameraData F-Stop Diyafram +cameraData ISO Speed ISO Hızı +cameraData Focal Length Odak Uzaklığı +cameraData Focal Length (35 mm) Odak Uzaklığı (35 mm) +cameraData Lens Lens + Activate Panel Paneli Etkinleştir + Change Panel Group Panel Grubunu Değiştir + Change Panel Layout Panel Düzenini Değiştir + Close Panel Paneli Kapat + Dock Panel Bağlantı İstasyonu Paneli + Float Panel Şamandıra Paneli + Move Panel Paneli Taşı + Group with Şununla grupla: + Panel menu Panel menüsü + Resize panel window Panel penceresini yeniden boyutlandır + Resize panels Panelleri yeniden boyutlandır + Use Classic Mode Klasik Modu Kullan + Auto Center Preview Otomatik Merkezleme Önizlemesi + Next Document Sonraki Belge + Previous Document Önceki Belge + W x H x Resolution G x Y x Çözünürlük + Front Image Ön Resim + Perspective Crop Tool Perspektif Kırpma Aracı + Drag a box or click four corners · drag the corners onto the edges of a skewed shape · {commit} commits · Esc cancels Bir kutuyu sürükleyin veya dört köşeyi tıklayın · köşeleri çarpık bir şeklin kenarlarına sürükleyin · {commit} taahhütleri · Esc iptalleri + Artboard Tool Çalışma Yüzeyi Aracı + Make Portrait Portre Yap + Make Landscape Manzara Yap + Add New Artboard Yeni Çalışma Yüzeyi Ekle + Freeform Pen Tool Serbest Kalem Aracı + Curvature Pen Tool Eğrilik Kalemi Aracı + Curve Fit: Eğri Fitili: + Drag: draw freehand · Release on the start point: close Sürükleyin: serbest çizim yapın · Başlangıç noktasında bırakın: kapat + Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Tıklama: düzgün nokta · Çift tıklama: köşe · Bir noktayı sürükleyin: taşıma · İlk noktayı tıklayın: kapat · {key} bitiş + New Crop Preset… Yeni Kırpma Ön Ayarı… + Delete Crop Preset… Kırpma Ön Ayarını Sil… + Select the crop presets to delete: Silinecek kırpma ön ayarlarını seçin: + Select the crop presets to delete Silinecek kırpma ön ayarlarını seçin + Enter a name for the crop preset Kırpma ön ayarı için bir ad girin + There is no crop size or ratio to save Kaydedilecek kırpma boyutu veya oranı yok + Search settings… Arama ayarları… + No matching settings Eşleşen ayar yok + PhotoCraft — Screen Color Picker PhotoCraft — Ekran Rengi Seçici + Loading fonts… Yazı tipleri yükleniyor… + Onion Skin Soğan Kabuğu + Marching Ants Yürüyen Karıncalar + On Black Siyah + On White Beyaz + On Layers Katmanlarda + Show Edge Kenarı Göster + Show Original Orijinali Göster + F cycles the views; hold X to see the image F görünümleri döndürür; resmi görmek için X'i basılı tutun + Refine Edge Brush (R) Kenar Fırçasını İncelt (R) + Expand Detection Area Algılama Alanını Genişlet + Restore Original Edge Orijinal Kenarı Geri Yükle + Paint over hair or fur on the image; Alt paints the other mode Görüntünün üzerine saç veya kürkün üzerini boyayın; Alt diğer modu boyar + Clear Brush Strokes Fırça Darbelerini Temizle + Show rulers Cetvelleri göster + Frame Tool Çerçeve Aracı + Create a new rectangular frame Yeni bir dikdörtgen çerçeve oluşturun + Create a new elliptical frame Yeni bir eliptik çerçeve oluşturun + Include in playback Oynatmaya dahil et + Scale: Ölçek: + Snap to Origin Orijine Yapış + Click to place · Alt-click or drag off-canvas to remove Yerleştirmek için tıklayın · Kaldırmak için Alt tuşuna basarak tıklayın veya tuvalin dışına sürükleyin + {n} of {max} sample points {n} / {max} örnek nokta + Emulsion Down Emülsiyon Aşağı + Functions Fonksiyonlar + Print Resolution: Baskı Çözünürlüğü: + Transparency Threshold Saydamlık Eşiği + Opacity Threshold Opaklık Eşiği + (No neutral color exists for {mode} mode.) ({mode} modu için nötr renk bulunmuyor.) + Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Sürükle: serbest çiz · {alt}-tıkla: düz segment · Başlangıç noktasında bırak: kapat + Fill with {mode}-neutral color (50% gray) {mode} nötr rengiyle doldur (%50 gri) + Fill with {mode}-neutral color (black) {mode} nötr rengiyle doldur (siyah) + Fill with {mode}-neutral color (white) {mode} nötr rengiyle doldur (beyaz) + Use Previous Layer to Create Clipping Mask Kırpma Maskesi Oluşturmak İçin Önceki Katmanı Kullan + Amount: Miktar: + Apply Vanishing Point ({key}) Ufuk Noktasını Uygula ({key}) + Apply the removal ({key}) Kaldırmayı uygula ({key}) + Drag to draw the curve · {key}-click draws a straight line Eğriyi çizmek için sürükle · {key}-tıklama düz çizgi çizer + Draw to modify the curve Eğriyi değiştirmek için çiz + Edit points to modify the curve Eğriyi değiştirmek için noktaları düzenle + Fix the red plane first: its corners must form a convex quad Önce kırmızı düzlemi düzeltin: köşeleri dışbükey bir dörtgen oluşturmalı + Grid Size: Izgara Boyutu: + Light Blue Açık Mavi + Light Gray Açık Gri + Light Red Açık Kırmızı + Medium Blue Orta Mavi + Merge Group Grubu Birleştir + Protect skin tones Ten tonlarını koru + Protect: Koru: + Remove after each stroke Her vuruştan sonra kaldır + Saving {percent} Kaydediliyor {percent} + Scale or rotate the moved area, then press {key} Taşınan alanı ölçeklendirin veya döndürün, ardından {key} tuşuna basın + Smooth the curve values Eğri değerlerini yumuşat + Strokes to remove: {n} (Enter applies, Esc cancels) Kaldırılacak vuruşlar: {n} (Enter uygular, Esc iptal eder) + The layer or its mask changed. Drag the mask onto the Trash again. Katman veya maskesi değişti. Maskeyi yeniden Çöp Kutusu'na sürükleyin. + Transform On Drop Bırakınca Dönüştür + Vanishing Point is not active Ufuk Noktası etkin değil + Vanishing Point: click four corners to create a plane Ufuk Noktası: düzlem oluşturmak için dört köşeye tıklayın +@plural Exported {n} swatch to {file}|Exported {n} swatches to {file} {n} renk örneği {file} dosyasına dışa aktarıldı +@plural Imported {n} brush from {group}|Imported {n} brushes from {group} {group} grubundan {n} fırça içe aktarıldı +@plural Imported {n} swatch from {file}|Imported {n} swatches from {file} {file} dosyasından {n} renk örneği içe aktarıldı +@plural Played {n} step|Played {n} steps {n} adım oynatıldı +@plural Saved for Web: {n} file|Saved for Web: {n} files Web İçin Kaydedildi: {n} dosya +@plural Updated {n} linked smart object changed on disk|Updated {n} linked smart objects changed on disk Diskte değişen {n} bağlantılı akıllı nesne güncellendi +@plural {n} layer recorded by this comp was deleted. Update the comp, or clear the warning.|{n} layers recorded by this comp were deleted. Update the comp, or clear the warning. Bu kompozisyonun kaydettiği {n} katman silindi. Kompozisyonu güncelleyin veya uyarıyı temizleyin. +@plural {n} plane|{n} planes {n} düzlem +@plural {n} preset|{n} presets {n} ön ayar +@plural {n} sample point removed|{n} sample points removed {n} örnek nokta kaldırıldı +@plural {n} slice|{n} slices {n} dilim +@plural {n} step|{n} steps {n} adım +@plural {warning} (+{n} more brush preset warning)|{warning} (+{n} more brush preset warnings) {warning} (+{n} fırça ön ayarı uyarısı daha) +@plural {what} with {n} warning|{what} with {n} warnings {what}, {n} uyarı ile diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 8b0ebf40ab..cba241e1be 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -48,7 +48,8 @@ catalog). 8 more, each at 1,608 / 1,608 measured keys, status `partial` for the same reason: Traditional Chinese `zh-hant` (2,329 rows), Russian `ru` (2,544), Ukrainian `uk` (3,375), Czech `cs` (3,522), Polish `pl` (3,366), Greek `el` (3,481), Dutch `nl` (3,568), Italian `it` (3,928; also every dynamic label the -other catalogs translate, [notes](localization-it.md)). +other catalogs translate, [notes](localization-it.md)). Turkish `tr` (3,731 rows) also covers every +measured key. Czech UI follow-up (2026-10-11): status and notice wrappers, hardware labels, brush and transform accessibility names, rasterization prompts, profile mismatch messages and preset @@ -79,3 +80,4 @@ engine localization or native review is complete. | 2026-10-11 | minor | Czech dynamic status/notice and hardware text, render labels and Navigator zoom context; engine messages remain English | | 2026-10-11 | minor | Czech review follow-ups: English System Info diagnostics, id-aware shortcut labels, display-only auto translation, warning punctuation and wording; catalog row count refreshed | | 2026-10-11 | minor | Italian: 1,287 dynamic labels added (filter parameters, Camera Raw, lighting, panels, dialogs) and terminology made consistent | +| 2026-10-12 | minor | Added Turkish (`tr`) catalog with 100% measured UI coverage | diff --git a/xtask/src/i18n_coverage.rs b/xtask/src/i18n_coverage.rs index 105f927435..2be43cb538 100644 --- a/xtask/src/i18n_coverage.rs +++ b/xtask/src/i18n_coverage.rs @@ -579,6 +579,7 @@ mod tests { ("el", "el.tsv"), ("uk", "uk.tsv"), ("nl", "nl.tsv"), + ("tr", "tr.tsv"), ("vi", "vi.tsv"), ] { let catalog_path = root.join("crates/ui-egui/src/i18n").join(path); From 9e623e7a4533063db3ff1f0968455236464d027e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bihacsy=20L=C3=A1szl=C3=B3?= <115165402+Skaywalker@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:07:17 +0200 Subject: [PATCH 23/27] i18n: add Hungarian (hu) UI translation (#3831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n(cs): drop duplicate catalog entries The Czech catalog listed Tool, Clone Stamp, History Brush, Dodge, Burn, Sponge and Stroke Path… twice, which fails `cargo xtask i18n-coverage` and the bundled_catalogs_are_consistent test. Keep one row each; for Stroke Path… keep the later row, the one the loader already used. * test(crop): pin the UI language to English in crop_default_mode The test queries widgets by their English labels ("Ratio", "Swaps height and width") but its harness followed the system locale, so it failed on a machine whose locale has a registered catalog. Set interface.language to "en" in the harness, as paint_bucket_all_layers and export_as_scaling do. * i18n: add Hungarian (hu) UI translation A complete Hungarian catalog (hu.tsv, 3908 entries: 3829 plain, 50 contextual, 21 plural, 8 command-id overrides) registered with the complete-menu languages; hu-HU locales resolve to it. Plural rule: plural_none. A Hungarian count takes the singular noun ("3 réteg"), so every @plural entry has one form. Clean room: written from the plain meaning of the English labels with ordinary Hungarian image-editing vocabulary; no proprietary localisation resource, string table or product UI was used. Also listed in the coverage test, LICENSE-translations.txt, ATTRIBUTION.md and docs/localization.md, with tag, system-locale and plural tests. --------- Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- ATTRIBUTION.md | 1 + .../ui-egui/src/i18n/LICENSE-translations.txt | 9 +- crates/ui-egui/src/i18n/hu.tsv | 3972 +++++++++++++++++ crates/ui-egui/src/i18n/mod.rs | 24 +- crates/ui-egui/src/i18n/system.rs | 1 + crates/ui-egui/tests/crop_default_mode.rs | 2 + docs/localization.md | 1 + xtask/src/i18n_coverage.rs | 1 + 8 files changed, 4006 insertions(+), 5 deletions(-) create mode 100644 crates/ui-egui/src/i18n/hu.tsv diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 7aece035b7..c2b9f87854 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -19,6 +19,7 @@ next to them. | `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/cs.tsv` | Czech UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | +| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `crates/ui-egui/src/i18n/tr.tsv` | Turkish UI translations | PhotoCraft contributors | Original translations of PhotoCraft's English labels | MIT OR Apache-2.0, [`LICENSE-translations.txt`](crates/ui-egui/src/i18n/LICENSE-translations.txt) | | `assets/fonts/Inter-Regular.ttf`, `Inter-Medium.ttf`, `Inter-SemiBold.ttf` | Inter 4.001 (UI font) | The Inter Project Authors (Rasmus Andersson) | | SIL OFL 1.1, [`assets/fonts/OFL-Inter.txt`](assets/fonts/OFL-Inter.txt) | diff --git a/crates/ui-egui/src/i18n/LICENSE-translations.txt b/crates/ui-egui/src/i18n/LICENSE-translations.txt index eae278e4dc..0177bcb56d 100644 --- a/crates/ui-egui/src/i18n/LICENSE-translations.txt +++ b/crates/ui-egui/src/i18n/LICENSE-translations.txt @@ -1,6 +1,6 @@ -PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch, Czech and Turkish UI translation catalogs are -licensed under MIT OR Apache-2.0, at your option. The full license texts are in ../../../../LICENSE-MIT -and ../../../../LICENSE-APACHE. +PhotoCraft's Korean, German, Brazilian Portuguese, Italian, Ukrainian, Dutch, Czech, Turkish and +Hungarian UI translation catalogs are licensed under MIT OR Apache-2.0, at your option. The full +license texts are in ../../../../LICENSE-MIT and ../../../../LICENSE-APACHE. Korean translations: PhotoCraft contributors, original translations of PhotoCraft's English labels; no proprietary translation resources were used. @@ -25,3 +25,6 @@ PhotoCraft's English labels; no proprietary translation resources were used. Turkish translations: PhotoCraft contributors, original translations of PhotoCraft's English labels; no proprietary translation resources were used. + +Hungarian translations: PhotoCraft contributors, original translations of +PhotoCraft's English labels; no proprietary translation resources were used. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv new file mode 100644 index 0000000000..080e204aaa --- /dev/null +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -0,0 +1,3972 @@ +# Hungarian (hu) catalog. Columns, separated by a TAB: context source translation +# +# context empty a plain UI string; `source` is the English text in the code +# @id a command id (e.g. file.saveAs): wins over the English text for +# that menu item, so one id can read differently from its twin +# @plural `source` is "one|other" (English forms); the translation lists the +# language's forms separated by "|" (see `plural` in i18n/mod.rs) +# anything else a disambiguating context passed to `tr_ctx` +# escapes \n \t \\ inside a column. `{name}` placeholders must appear in both columns. +# lines starting with # and blank lines are ignored. +# +# Clean-room: every entry was written from the plain meaning of the English label using ordinary +# Hungarian image-editing vocabulary (réteg, maszk, kijelölés ...). No proprietary localisation +# resource, string table or product UI was consulted or copied; add entries the same way. +# Product and technology names (PhotoCraft, ArtCraft, OpenType, CMYK, Lab ...) stay in Latin +# letters; the trailing "…" of dialog commands is kept. Plural rule: a count is followed by the +# singular noun in Hungarian, so every @plural entry has a single form. + +# Plural messages and command id overrides +@id edit.purge.undo Visszavonási adatok +@id edit.stroke Körvonalazás… +@id edit.transform.scale Méretezés +@id filter.sharpen.sharpen Egyszerű élesítés +@id select.all Mind +@plural Exported {n} swatch to {file}|Exported {n} swatches to {file} {n} színminta exportálva. Cél: {file} +@plural Group · {n} layer|Group · {n} layers Csoport · {n} réteg +@plural Imported {n} brush from {file}|Imported {n} brushes from {file} {n} ecset importálva. Forrás: {file} +@plural Imported {n} brush from {group}|Imported {n} brushes from {group} {n} ecset importálva. Csoport: {group} +@plural Imported {n} gradient from {file}|Imported {n} gradients from {file} {n} színátmenet importálva. Forrás: {file} +@plural Imported {n} swatch from {file}|Imported {n} swatches from {file} {n} színminta importálva. Forrás: {file} +@plural Played {n} step|Played {n} steps {n} lépés lejátszva +@plural Saved for Web: {n} file|Saved for Web: {n} files Mentés webre: {n} fájl +@plural Updated {n} linked smart object changed on disk|Updated {n} linked smart objects changed on disk {n} csatolt intelligens objektum frissítve (a lemezen módosult) +@plural {n} item|{n} items {n} elem +@plural {n} layer recorded by this comp was deleted. Update the comp, or clear the warning.|{n} layers recorded by this comp were deleted. Update the comp, or clear the warning. A rétegkompozícióban rögzített rétegek közül {n} törlődött. Frissítsd a rétegkompozíciót, vagy töröld a figyelmeztetést. +@plural {n} Layer|{n} Layers {n} réteg +@plural {n} layer|{n} layers {n} réteg +@plural {n} plane|{n} planes {n} sík +@plural {n} preset|{n} presets {n} előbeállítás +@plural {n} sample point removed|{n} sample points removed {n} mintavételi pont eltávolítva +@plural {n} sample|{n} samples {n} minta +@plural {n} slice|{n} slices {n} szelet +@plural {n} step|{n} steps {n} lépés +@plural {warning} (+{n} more brush preset warning)|{warning} (+{n} more brush preset warnings) {warning} (+{n} további figyelmeztetés az ecset-előbeállításokról) +@plural {what} with {n} warning|{what} with {n} warnings {what} ({n} figyelmeztetés) +@id file.close Bezárás +@id layer.layerMask.apply Alkalmazás +@id edit.clear Kiürítés + +# Menus: File, Edit, Image + 16 Bits/Channel 16 bit/csatorna + 180° 180° + 32 Bits/Channel 32 bit/csatorna + 8 Bits/Channel 8 bit/csatorna + 90° Clockwise 90° az óramutató járása szerint + 90° Counter Clockwise 90° az óramutató járásával szemben + Adjustments Korrekciók + Again Ismétlés + AI Integrations… MI-integrációk… + Analysis Elemzés + Apply Data Set… Adatkészlet alkalmazása… + Apply Image… Kép alkalmazása… + Arbitrary… Tetszőleges… + Artboards to Files… Rajztáblák fájlokba… + Artboards to PDF… Rajztáblák PDF-be… + Assign Profile… Profil hozzárendelése… + Auto Color Automatikus szín + Auto Contrast Automatikus kontraszt + Auto Tone Automatikus tónus + Auto-Align Layers… Rétegek automatikus igazítása… + Auto-Blend Layers… Rétegek automatikus keverése… + Automate Automatizálás + Batch… Kötegelt feldolgozás… + Bitmap Bittérkép + Black & White… Fekete-fehér… + Brightness/Contrast… Fényerő/kontraszt… + Browse… Tallózás… + Calculations… Számítások… + Canvas Size… Vászonméret… + Channel Mixer… Csatornakeverő… + Check Spelling… Helyesírás-ellenőrzés… + Clipboard Vágólap + Close All Összes bezárása + Close Others Többi bezárása + Color Balance… Színegyensúly… + Color Lookup Tables… Színleképezési táblák… + Color Lookup… Színleképezés… + Color Settings… Színbeállítások… + Color Table… Színtábla… + Conditional Mode Change… Feltételes módváltás… + Contact Sheet II… Kontaktlap II… + Content-Aware Fill… Tartalomérzékeny kitöltés… + Content-Aware Scale Tartalomérzékeny méretezés + Convert to Profile… Átalakítás profilra… + Copy Másolás + Copy Merged Egyesített másolás + Create Droplet… Droplet létrehozása… + Crop Körbevágás + Crop and Straighten Photos Fotók körbevágása és kiegyenesítése + Cursors… Kurzorok… + Curves… Görbék… + Cut Kivágás + Data Sets as Files… Adatkészletek fájlokként… + Data Sets… Adatkészletek… + Define Brush Preset… Ecset-előbeállítás megadása… + Define Custom Shape… Egyéni alakzat megadása… + Define Pattern… Mintázat megadása… + Define… Megadás… + Delete All Empty Layers Összes üres réteg törlése + Desaturate Színtelenítés + Distort Torzítás + Duotone Kéttónusú + Duplicate… Kettőzés… + Edit Szerkesztés + Enhanced Controls… Bővített vezérlők… + Equalize Kiegyenlítés + Exit Kilépés + Export As… Exportálás másként… + Export Preferences… Beállítások exportálása… + Export/Import Presets… Előbeállítások exportálása/importálása… + Export… Exportálás… + Exposure… Expozíció… + Fade… Halványítás… + File Fájl + File Handling… Fájlkezelés… + File Info… Fájladatok… + Fill… Kitöltés… + Find and Replace Text… Szöveg keresése és cseréje… + Fit Image… Kép méretre igazítása… + Flatten All Layer Effects Összes rétegeffektus egybesimítása + Flatten All Masks Összes maszk egybesimítása + Flip Canvas Horizontal Vászon vízszintes tükrözése + Flip Canvas Vertical Vászon függőleges tükrözése + Flip Horizontal Vízszintes tükrözés + Flip Vertical Függőleges tükrözés + Free Transform Szabad alakítás + Generate Létrehozás + Gradient Map… Színátmenet-leképezés… + Guides, Grid & Slices… Segédvonalak, rács és szeletek… + HDR Toning… HDR-tónusozás… + Histories Előzmények + History Log… Előzménynapló… + Hue/Saturation… Színárnyalat/telítettség… + Image Kép + Image Assets Képelemek + Image Processor… Képfeldolgozó… + Image Rotation Képforgatás + Indexed Color Indexelt szín + Interface… Felület… + Keyboard Shortcuts… Billentyűparancsok… + Layer Comps to Files… Rétegkompozíciók fájlokba… + Layers to Files… Rétegek fájlokba… + Lens Correction… Objektívkorrekció… + Levels… Szintek… + Load Files into Stack… Fájlok betöltése halomba… + Match Color… Színillesztés… + Menus… Menük… + Merge to HDR Pro… HDR Pro-egyesítés… + Migrate Presets Előbeállítások átvétele + Multichannel Többcsatornás + New… Új… + Notes… Jegyzetek… + Open As… Megnyitás mint… + Open EXR Parts… EXR-részek megnyitása… + Open… Megnyitás… + Package… Csomagolás… + Paste Beillesztés + Paste in Place Beillesztés a helyére + Paste Into Beillesztés belülre + Paste Outside Beillesztés kívülre + Paste Special Speciális beillesztés + Paths to Illustrator… Útvonalak Illustrator-fájlba… + Performance… Teljesítmény… + Perspective Perspektíva + Perspective Warp Perspektivikus torzítás + Photo Filter… Fotószűrő… + Photomerge… Fotóegyesítés… + Place Embedded… Beágyazott elhelyezése… + Place Linked… Csatolt elhelyezése… + Place Scale Marker… Méretarány-jelölő elhelyezése… + Plug-ins… Bővítmények… + Posterize… Poszterizálás… + Preferences Beállítások + Preset Manager… Előbeállítás-kezelő… + Presets Előbeállítások + Print One Copy Egy példány nyomtatása + Print… Nyomtatás… + Puppet Warp Bábtorzítás + Purge Memória ürítése + Quick Export as PNG Gyors exportálás PNG-be + Raw Defaults… Raw-alapértékek… + Record Measurements Mérések rögzítése + Redo Újra + Remove Warp Split Hajlításfelosztás eltávolítása + Render Video… Videó renderelése… + Replace Color… Színcsere… + Reveal All Minden megjelenítése + Revert Visszaállítás + Rotate Forgatás + Rotate 180° Forgatás 180°-kal + Rotate 90° Clockwise Forgatás 90°-kal az óramutató járása szerint + Rotate 90° Counter Clockwise Forgatás 90°-kal az óramutató járásával szemben + Save Mentés + Save a Copy… Másolat mentése… + Save As… Mentés másként… + Save for Web (Legacy)… Mentés webre (régi)… + Scale Méretarány + Scratch Disks… Lapozólemezek… + Script Events Manager… Parancsfájl-eseménykezelő… + Scripts Parancsfájlok + Search… Keresés… + Select Data Points… Adatpontok kiválasztása… + Selective Color… Szelektív szín… + Set Measurement Scale… Mérési méretarány beállítása… + Settings… Beállítások… + Shadows/Highlights… Árnyékok/csúcsfények… + Skew Döntés + Split Warp Crosswise Hajlítás felosztása keresztben + Split Warp Horizontally Hajlítás felosztása vízszintesen + Split Warp Vertically Hajlítás felosztása függőlegesen + Statistics… Statisztika… + Stroke… Körvonal… + Threshold… Küszöb… + Toggle Last State Utolsó állapot váltása + Toolbar… Eszköztár… + Tools… Eszközök… + Transform Alakítás + Transparency & Gamut… Átlátszóság és színtartomány… + Trap… Ráfutás… + Trim… Levágás… + Type… Szöveg… + Undo Visszavonás + Units & Rulers… Mértékegységek és vonalzók… + Variable Data Sets… Változó adatkészletek… + Variables Változók + Vibrance… Élénkség… + Video Cache Videogyorsítótár + Video Frames to Layers… Videókockák rétegekbe… + Warp Hajlítás + WIA Support… WIA-támogatás… + Workspace… Munkaterület… + +# Menus: Layer, Select + Align Igazítás + Apply Alkalmaz + Arrange Elrendezés + Artboard from Group… Rajztábla csoportból… + Artboard from Layers… Rajztábla rétegekből… + Artboard… Rajztábla… + Background from Layer Háttér a rétegből + Bevel & Emboss… Fazetta és domborítás… + Blending Options… Keverési beállítások… + Border… Szegély… + Bottom Edges Alsó szélek + Bring Forward Előrébb hozás + Bring to Front Előrehozás legfelülre + Clear Layer Style Rétegstílus törlése + Clear Smart Filters Intelligens szűrők törlése + Color Decontaminate… Színszennyeződés eltávolítása… + Color Overlay… Színfedés… + Color Range… Színtartomány… + Combine Shapes Alakzatok kombinálása + Contract… Szűkítés… + Convert to Embedded Átalakítás beágyazottá + Convert to Layers Átalakítás rétegekké + Convert to Linked… Átalakítás csatolttá… + Convert to Smart Object Átalakítás intelligens objektummá + Copy Layer Style Rétegstílus másolása + Create Clipping Mask Vágómaszk létrehozása + Create Layer Réteg létrehozása + Current Path Aktuális útvonal + Defringe… Szegélyjavítás… + Delete Filter Mask Szűrőmaszk törlése + Delete Frame Képkocka törlése + Deselect Kijelölés megszüntetése + Deselect Layers Rétegek kijelölésének megszüntetése + Disable Filter Mask Szűrőmaszk letiltása + Disable Smart Filters Intelligens szűrők letiltása + Distribute Elosztás + Drop Shadow… Vetett árnyék… + Duplicate Frame Képkocka kettőzése + Duplicate Layer… Réteg kettőzése… + Edit Contents Tartalom szerkesztése + Edit in Quick Mask Mode Szerkesztés gyorsmaszk módban + Enable Layer Mask Rétegmaszk engedélyezése + Enable Vector Mask Vektormaszk engedélyezése + Entropy Entrópia + Exclude Overlapping Shapes Átfedő alakzatok kizárása + Expand… Bővítés… + Export Contents… Tartalom exportálása… + Feather… Elmosott szél… + Fill Content Kitöltés tartalma + Find Layers Rétegek keresése + Flatten Image Kép egybesimítása + Focus Area… Fókuszterület… + Frame from Layers Keret rétegekből + From Transparency Átlátszóságból + Global Light… Globális fény… + Gradient Overlay… Színátmenet-fedés… + Gradient… Színátmenet… + Group from Layers… Csoport rétegekből… + Group Layers Rétegek csoportosítása + Group… Csoport… + Grow Növelés + Hidden Layers Rejtett rétegek + Hide All Minden elrejtése + Hide All Effects Összes effektus elrejtése + Hide Layers Rétegek elrejtése + Hide Selection Kijelölés elrejtése + Horizontal Centers Vízszintes középvonalak + Inner Glow… Belső ragyogás… + Inner Shadow… Belső árnyék… + Insert Blank Frame Üres képkocka beszúrása + Interpret Footage… Videoanyag értelmezése… + Intersect Shape Areas Alakzatterületek metszete + Inverse Selection Kijelölés megfordítása + Invert… Invertálás… + Isolate Layers Rétegek elkülönítése + Kurtosis Csúcsosság + Layer Réteg + Layer Content Options… Rétegtartalom beállításai… + Layer from Background… Réteg a háttérből… + Layer Style Rétegstílus + Layer via Copy Réteg másolással + Layer via Cut Réteg kivágással + Layer… Réteg… + Left Edges Bal szélek + Link Layer Mask Rétegmaszk összekapcsolása + Link Layers Rétegek összekapcsolása + Link Vector Mask Vektormaszk összekapcsolása + Load Selection… Kijelölés betöltése… + Lock Layers… Rétegek zárolása… + Mask All Objects Összes objektum maszkolása + Matting Szegélykezelés + Maximum Maximum + Mean Átlag + Median Medián + Merge Layers Rétegek egyesítése + Merge Shape Components Alakzatösszetevők egyesítése + Merge Visible Láthatók egyesítése + Modify Módosítás + New Adjustment Layer Új korrekciós réteg + New Blank Video Layer Új üres videoréteg + New Fill Layer Új kitöltőréteg + New Layer Based Slice Új rétegalapú szelet + New Smart Object via Copy Új intelligens objektum másolással + New Video Layer from File… Új videoréteg fájlból… + Outer Glow… Külső ragyogás… + Paste Layer Style Rétegstílus beillesztése + Pattern Overlay… Mintázatfedés… + Pattern… Mintázat… + Range Terjedelem + Rasterize Raszterizálás + Relink to File… Újracsatolás fájlhoz… + Reload Frame Képkocka újratöltése + Remove Black Matte Fekete szegély eltávolítása + Remove White Matte Fehér szegély eltávolítása + Rename Layer Réteg átnevezése + Replace Contents… Tartalom cseréje… + Replace Footage… Videoanyag cseréje… + Reselect Újbóli kijelölés + Restore All Frames Összes képkocka visszaállítása + Restore Frame Képkocka visszaállítása + Reveal in Finder Megjelenítés a Finderben + Reveal Selection Kijelölés megjelenítése + Right Edges Jobb szélek + Satin… Szatén… + Save Selection… Kijelölés mentése… + Scale Effects… Effektusok méretezése… + Select Kijelölés + Select and Mask… Kijelölés és maszk… + Select Linked Layers Összekapcsolt rétegek kijelölése + Send Backward Hátrébb küldés + Send to Back Hátraküldés legalulra + Show Altered Video Módosított videó megjelenítése + Similar Hasonló + Skewness Ferdeség + Sky Égbolt + Smart Filter Intelligens szűrő + Smart Object Intelligens objektum + Smart Objects Intelligens objektumok + Smooth… Simítás… + Solid Color… Egyszínű kitöltés… + Stack Mode Halommód + Standard Deviation Szórás + Subject Fő téma + Subtract Front Shape Elülső alakzat kivonása + Summation Összegzés + Top Edges Felső szélek + Transform Selection Kijelölés alakítása + Type Szöveg + Ungroup Layers Csoport bontása + Unite Shapes Alakzatok egyesítése + Update All Modified Content Összes módosult tartalom frissítése + Update Modified Content Módosult tartalom frissítése + Variance Variancia + Vector Mask Vektormaszk + Vertical Centers Függőleges középvonalak + Video Videó + Video Layers Videorétegek + +# Menus: Type, Filter, View, Window; UI and engine commands + (coming soon) (hamarosan) + 100% 100% + 2-up Horizontal 2 nézet vízszintesen + 2-up Vertical 2 nézet függőlegesen + 200% 200% + 3-up Horizontal 3 nézet vízszintesen + 3-up Stacked 3 nézet egymás alatt + 3-up Vertical 3 nézet függőlegesen + 32-bit Preview Options… 32 bites előnézet beállításai… + 4-up 4 nézet + 6-up 6 nézet + About PhotoCraft A PhotoCraft névjegye + About {app} Névjegy: {app} + Actions Műveletsorok + Activate Panel Panel aktiválása + Actual Pixels Tényleges méret + Adaptive Wide Angle… Adaptív nagy látószög… + Add Layer Mask Rétegmaszk hozzáadása + Add Noise… Zaj hozzáadása… + Add Vector Mask Vektormaszk hozzáadása + Adwaita Dark Theme Adwaita sötét téma + Adwaita Light Theme Adwaita világos téma + AI Integrations MI-integrációk + Anamorphic 2:1 (2) Anamorfikus 2:1 (2) + Anti-Alias Élsimítás + Appearance Megjelenés + Apply Data Set Adatkészlet alkalmazása + Apply Layer Mask Rétegmaszk alkalmazása + Artboard Guides Rajztábla-segédvonalak + Artboards to Files Rajztáblák fájlokba + Artboards to PDF Rajztáblák PDF-be + ArtCraft Website ArtCraft-weboldal + Average Átlag + Background From Layer Háttér a rétegből + Bitmap… Bittérkép… + Blur Elmosás + Blur Gallery Elmosásgaléria + Blur More Erősebb elmosás + Box Blur… Dobozelmosás… + Breeze Dark Theme Breeze sötét téma + Breeze Light Theme Breeze világos téma + Brush Preview Ecset előnézete + Brush Settings Ecsetbeállítások + Brushes Ecsetek + brushes ecsetek + Camera Raw Filter… Camera Raw szűrő… + Canvas Guides Vászon-segédvonalak + Cascade Lépcsőzetes + Change Panel Group Panelcsoport váltása + Change Panel Layout Panelelrendezés váltása + Change… Módosítás… + Channels Csatornák + Character Panel Karakter panel + Character Styles Karakterstílusok + Character Styles Panel Karakterstílusok panel + Classic Theme Klasszikus téma + Clear Canvas Guides Vászon-segédvonalak törlése + Clear Guides Segédvonalak törlése + Clear Recent Files Legutóbbi fájlok listájának törlése + Clear Selected Artboard Guides Kijelölt rajztábla segédvonalainak törlése + Clear Slices Szeletek törlése + Clipping Mask Vágómaszk + Clipping Path… Vágóútvonal… + Clone Source Klónozási forrás + Close Panel Panel bezárása + Close Window Ablak bezárása + Clouds Felhők + Color Blindness — Deuteranopia-type Színtévesztés — deuteranópia típusú + Color Blindness — Protanopia-type Színtévesztés — protanópia típusú + Color Halftone… Színes féltónus… + Color Lookup Tables Színleképezési táblák + Color Picker (Custom Canvas Color) Színválasztó (egyéni vászonszín) + Color to Alpha… Szín átalakítása átlátszóvá… + Consolidate All to Tabs Összes lapfülre + Contextual Alternates Környezetfüggő változatok + Convert for Smart Filters Átalakítás intelligens szűrőkhöz + Convert to Paragraph Text Átalakítás bekezdésszöveggé + Convert to Point Text Átalakítás pontszöveggé + Convert to Shape Átalakítás alakzattá + Copy Complete Stroke Teljes körvonal másolása + Copy Fill Kitöltés másolása + Count Darabszám + Create Vector Mask Vektormaszk létrehozása + Create Work Path Munkaútvonal létrehozása + Crisp Tiszta + Crosswise Keresztben + Crystallize… Kristályosítás… + Custom Pixel Aspect Ratio Egyéni pixelarány + Custom Pixel Aspect Ratio… Egyéni pixelarány… + Custom… Egyéni… + Cycle Screen Mode Képernyőmód váltása + D1/DV NTSC (0.91) D1/DV NTSC (0,91) + D1/DV NTSC Widescreen (1.21) D1/DV NTSC szélesvásznú (1,21) + D1/DV PAL (1.09) D1/DV PAL (1,09) + D1/DV PAL Widescreen (1.46) D1/DV PAL szélesvásznú (1,46) + Dark gray Sötétszürke + Dark Gray Sötétszürke + Data Sets as Files Adatkészletek fájlokként + De-Interlace… Váltott soros megjelenítés megszüntetése… + Default Features Alapértelmezett funkciók + Define Brush Preset Ecset-előbeállítás megadása + Define Custom Shape Egyéni alakzat megadása + Define Pattern Mintázat megadása + Delete and Fill Selection Kijelölés törlése és kitöltése + Delete Layer Réteg törlése + Delete Layer Mask Rétegmaszk törlése + Delete Layers Rétegek törlése + Delete Vector Mask Vektormaszk törlése + Delete Workspace… Munkaterület törlése… + Despeckle Szemcsék eltávolítása + Difference Clouds Különbségfelhők + Diffuse… Diffúzió… + Disable Layer Mask Rétegmaszk letiltása + Disable Symmetry Path Szimmetriaútvonal letiltása + Discretionary Ligatures Választható ligatúrák + Displace… Elmozdítás… + Dock Panel Panel dokkolása + Document Bounds Dokumentum határai + Duotone… Kéttónusú… + Duplicate Layers… Rétegek kettőzése… + Dust & Scratches… Por és karcolások… + DVCPRO HD 1080/HDV 1080 (1.5) DVCPRO HD 1080/HDV 1080 (1,5) + East Asian Features Kelet-ázsiai funkciók + Edit Pins Tűk szerkesztése + Edit Type Szöveg szerkesztése + Effects Effektusok + Emboss… Domborítás… + Essentials Alapok + Extra Large Nagyon nagy + Extras Segédelemek + Extrude… Kihúzás… + F F + Facet Lapolás + Fibers… Szálak… + Field Blur… Mezőelmosás… + Fill Path… Útvonal kitöltése… + Fill Screen Képernyő kitöltése + Filter Szűrő + Filter Gallery… Szűrőgaléria… + Find and Replace Text Szöveg keresése és cseréje + Fit Artboard on Screen Rajztábla képernyőhöz igazítása + Fit Layer(s) on Screen Réteg(ek) képernyőhöz igazítása + Fit on Screen Képernyőhöz igazítás + Flame… Láng… + Float All in Windows Összes lebegő ablakban + Float in Window Lebegő ablakban + Float Panel Panel lebegtetése + Font Preview Size Betűtípus-előnézet mérete + Fractions Törtek + Fragment Töredezés + Frame from Layers… Keret rétegekből… + Free Transform a Copy Másolat szabad alakítása + Free Transform Path Útvonal szabad alakítása + Full Screen Teljes képernyő + Full Screen Mode Teljes képernyős mód + Full Screen Mode With Menu Bar Teljes képernyős mód menüsorral + Gamut Warning Színtartomány-figyelmeztetés + Gaussian Blur… Gauss-elmosás… + Glyphs Glifák + Glyphs Panel Glifák panel + Gradients Színátmenetek + Graphic and Web Grafika és web + Graphic and web Grafika és web + Grid Rács + Group from Layers Csoport rétegekből + Guides Segédvonalak + HDV 1080/DVCPRO HD 720 (1.33) HDV 1080/DVCPRO HD 720 (1,33) + Help Súgó + Hide Elrejtés + Hide Others Többi elrejtése + Hide {app} Elrejtés: {app} + High Pass… Felüláteresztő… + Histogram Hisztogram + History Előzmények + Horizontal Vízszintes + HSB/HSL HSB/HSL + Huge Óriási + Image Processor Képfeldolgozó + Indexed Color… Indexelt szín… + Info Információ + Install Plug-in Bővítmény telepítése + Install Plug-in… Bővítmény telepítése… + Internet Standard RGB Internetes szabványos RGB + Inverse Megfordítás + Iris Blur… Íriszelmosás… + Join the ArtCraft Discord Csatlakozás az ArtCraft Discordhoz + Join the ArtCraft Discord… Csatlakozás az ArtCraft Discordhoz… + Language Nyelv + Language Options Nyelvi beállítások + Large Nagy + Last Filter Utolsó szűrő + Layer Comps Rétegkompozíciók + Layer Comps to Files Rétegkompozíciók fájlokba + Layer Content Options Rétegtartalom beállításai + Layer Edges Rétegszélek + Layer from Background Réteg a háttérből + Layer From Background… Réteg a háttérből… + Layers Rétegek + Layers to Files Rétegek fájlokba + Legacy Macintosh RGB Régi Macintosh RGB + Lens Blur… Objektívelmosás… + Lens Flare… Lencsecsillanás… + Light gray Világosszürke + Light Gray Világosszürke + Lighting Effects… Fényhatások… + Liquify… Képlékenyítés… + Load Default Type Styles Alapértelmezett szövegstílusok betöltése + Load Files into Stack Fájlok betöltése halomba + Lock Guides Segédvonalak zárolása + Lock Slices Szeletek zárolása + Lock Workspace Munkaterület zárolása + Make Selection… Kijelölés készítése… + Make Symmetry Path Szimmetriaútvonal készítése + Make Work Path… Munkaútvonal készítése… + Match All Összes illesztése + Match Location Hely illesztése + Match Rotation Elforgatás illesztése + Match Zoom Nagyítás illesztése + Maximum… Maximum… + Measurement Log Mérési napló + Median… Medián… + Medium gray Középszürke + Medium Gray Középszürke + Merge Down Egyesítés lefelé + Merge to HDR Pro HDR Pro-egyesítés + Mesh Háló + Mezzotint… Mezzotinto… + Middle Eastern & South Asian Composer Közel-keleti és dél-ázsiai szedőmotor + Middle Eastern Features Közel-keleti funkciók + Minimize Minimalizálás + Minimum… Minimum… + Modifier Keys Módosítóbillentyűk + Monitor RGB Monitor-RGB + Mosaic… Mozaik… + Motion Mozgókép + Motion Blur… Mozgási elmosás… + Move Panel Panel áthelyezése + Navigator Navigátor + New from Clipboard Új a vágólapról + New Guide Layout… Új segédvonal-elrendezés… + New Guides From Shape Új segédvonalak alakzatból + New Guide… Új segédvonal… + New Layer… Új réteg… + New Shape Layer Új alakzatréteg + New Type Layer Új szövegréteg + New Video Layer Új videoréteg + New Window Új ablak + New Window for Document Új ablak a dokumentumhoz + New Workspace… Új munkaterület… + Next Appearance Mode Következő megjelenési mód + Next Document Következő dokumentum + Next Theme Következő téma + No matching commands Nincs egyező parancs + Notes Jegyzetek + NTSC Colors NTSC-színek + Offset… Eltolás… + Oil Paint… Olajfestmény… + Oldstyle Régies számok + Open As Megnyitás mint + Open Recent Legutóbbi megnyitása + Open Recent is unavailable on the web A legutóbbi fájlok listája a webes változatban nem érhető el + OpenType OpenType + Options Beállítósáv + Ordinals Sorszámnevek + Orientation Tájolás + Ornaments Díszek + Other… Egyéb… + Painting Festés + Panels Panelek + Paragraph Panel Bekezdés panel + Paragraph Styles Bekezdésstílusok + Paragraph Styles Panel Bekezdésstílusok panel + Paste Complete Stroke Teljes körvonal beillesztése + Paste Fill Kitöltés beillesztése + Paste Lorem Ipsum Lorem ipsum beillesztése + Path Blur… Útvonal-elmosás… + Paths Útvonalak + Paths to Illustrator Útvonalak Illustrator-fájlba + Pattern Preview Mintázat előnézete + PhotoCraft PhotoCraft + PhotoCraft on GitHub PhotoCraft a GitHubon + PhotoCraft Website PhotoCraft-weboldal + Photography Fotózás + Picture Frame… Képkeret… + Pinch… Becsípés… + Pixel Art Pixelgrafika + Pixel Art Preview Pixelgrafikus előnézet + Pixel Aspect Ratio Pixelarány + Pixel Aspect Ratio Correction Pixelarány-korrekció + Pixel Grid Pixelrács + Pixelate Pixelesítés + Plug-ins Bővítmények + Pointillize… Pontozás… + Polar Coordinates… Polárkoordináták… + Preferences… Beállítások… + Previous Document Előző dokumentum + Print Size Nyomtatási méret + Pro Light Gray Theme Pro világosszürke téma + Pro Light Theme Pro világos téma + Pro Medium Gray Theme Pro középszürke téma + Pro Theme Pro téma + Proof Colors Színpróba + Proof Setup Színpróba beállítása + Properties Tulajdonságok + Quick Export As PNG Gyors exportálás PNG-be + Quit Kilépés + Quit {app} Kilépés: {app} + Radial Blur… Sugárirányú elmosás… + Rasterize Layer Réteg raszterizálása + Rasterize Layer Style Rétegstílus raszterizálása + Rasterize Layers Rétegek raszterizálása + Rasterize Shape Alakzat raszterizálása + Rasterize Type Szöveg raszterizálása + Rasterize Type Layer Szövegréteg raszterizálása + Raw Defaults Raw-alapértékek + Reduce Noise… Zajcsökkentés… + Release Clipping Mask Vágómaszk feloldása + Relight… Újravilágítás… + Rename Layer… Réteg átnevezése… + Render Renderelés + Replace All Missing Fonts Összes hiányzó betűtípus cseréje + Report an Issue Hiba bejelentése + Report an Issue… Hiba bejelentése… + Reset All Összes alaphelyzetbe + Reset All Tools Összes eszköz alaphelyzetbe + Reset Tool Eszköz alaphelyzetbe + Reset View Nézet alaphelyzetbe + Reset Workspace Munkaterület alaphelyzetbe + Resolve Missing Fonts Hiányzó betűtípusok kezelése + Resolve Missing Fonts… Hiányzó betűtípusok kezelése… + Ripple… Fodrozódás… + Rotate View Nézetforgatás + Rulers Vonalzók + Save As Mentés másként + Save Default Type Styles Alapértelmezett szövegstílusok mentése + Screen Mode Képernyőmód + Scroll menu down Menü görgetése lefelé + Scroll menu up Menü görgetése felfelé + Search menus Keresés a menükben + Select All Összes kijelölése + Select Custom Color… Egyéni szín kiválasztása… + Select Inverse Kijelölés megfordítása + Selection Edges Kijelölésszélek + Settings Beállítások + Shape Blur… Alakzatos elmosás… + Shapes Alakzatok + Sharp Éles + Sharpen Élesítés + Sharpen Edges Élek élesítése + Sharpen More Erősebb élesítés + Shear… Nyírás… + Show Megjelenítés + Show All Összes megjelenítése + Show Extras Options… Segédelemek beállításai… + Show in Explorer Megjelenítés az Intézőben + Show in Folder Megjelenítés mappában + Show menu colors Menüszínek megjelenítése + Show/Hide All Panels Összes panel megjelenítése/elrejtése + Show/Hide Panels Panelek megjelenítése/elrejtése + Simulate Pressure Nyomás szimulálása + Slices Szeletek + Small Kicsi + Smart Blur… Intelligens elmosás… + Smart Guides Intelligens segédvonalak + Smart Sharpen… Intelligens élesítés… + Smooth Sima + Snap Illesztés + Snap To Illesztési célok + Solarize Szolarizáció + Solarized Dark Theme Solarized sötét téma + Spherize… Gömbösítés… + Spin Blur… Pörgetéses elmosás… + Square Négyzetes + Standard Ligatures Normál ligatúrák + Standard Screen Mode Normál képernyőmód + Statistics Statisztika + Stroke Path… Útvonal körvonalazása… + Strong Erős + Studio Light Theme Studio világos téma + Studio Theme Studio téma + Styles Stílusok + Stylistic Alternates Stilisztikai változatok + Stylize Stilizálás + Subtract Shapes at Overlap Alakzatok kivonása az átfedésben + Surface Blur… Felületi elmosás… + Swash Díszbetűk + Swatches Színminták + Sync with system Rendszer szerint + System Info… Rendszerinformáció… + Target Path Célútvonal + Theme Téma + Tile Mozaikelrendezés + Tile All Horizontally Összes vízszintes mozaikba + Tile All Vertically Összes függőleges mozaikba + Tiles… Csempék… + Tilt-Shift… Tilt-shift… + Timeline Idővonal + Titling Alternates Címbetű-változatok + Tool Presets Eszköz-előbeállítások + Tools Eszközök + Trace Contour… Kontúrkövetés… + Transform Symmetry Szimmetria alakítása + Tree… Fa… + Twirl… Örvény… + Unite Shapes at Overlap Alakzatok egyesítése az átfedésben + Unlink Layer Mask Rétegmaszk szétkapcsolása + Unsharp Mask… Életlenmaszk… + Update All Text Layers Összes szövegréteg frissítése + Vanishing Point… Enyészpont… + Variable Data Sets Változó adatkészletek + Vector Mask from Current Path Vektormaszk az aktuális útvonalból + View menu Nézet menü + Warp Text… Szövegtorzítás… + Wave… Hullám… + WIA Support WIA-támogatás + Window Ablak + Windows Windows + Windows LCD Windows LCD + Wind… Szél… + Working Black Plate Fekete munkalemez + Working CMY Plate CMY munkalemezek + Working CMYK Munka-CMYK + Working Cyan Plate Cián munkalemez + Working Magenta Plate Magenta munkalemez + Working Yellow Plate Sárga munkalemez + Workspace Munkaterület + ZigZag… Cikcakk… + Zoom In Nagyítás + Zoom Out Kicsinyítés + +# Tools, brushes and the options bar + 101 by 101 Average 101 x 101 átlag + 11 by 11 Average 11 x 11 átlag + 3 by 3 Average 3 x 3 átlag + 3 x 3 3 x 3 + 31 by 31 Average 31 x 31 átlag + 4 x 4 4 x 4 + 5 by 5 Average 5 x 5 átlag + 5 x 5 5 x 5 + 51 by 51 Average 51 x 51 átlag + Add Hozzáad + Add New Artboard Új rajztábla hozzáadása + Add pair Pár hozzáadása + Add space after paragraph Térköz a bekezdés után + Add space before paragraph Térköz a bekezdés előtt + Adds the current gradient to the Gradients panel Hozzáadja a jelenlegi színátmenetet a Színátmenetek panelhez + Adjust for Zoom Igazítás a nagyításhoz + Adjustment layers have nothing to transform A korrekciós rétegeken nincs mit alakítani + Airbrush: keeps depositing paint while the pointer rests. Festékszóró: a festék akkor is gyűlik, amikor a mutató egy helyben áll. + Align with layer Igazítás a réteghez + Aligned Igazított + Alignment Igazítás + All Caps Csupa nagybetű + All Layers Minden réteg + All Layers No Adjustments Minden réteg, korrekciók nélkül + Alt Alt + Alt-click to define a source point to clone from Alt+kattintással add meg a klónozás forráspontját + Alternative Alternatív + Always Mindig + Always Show Overlay Fedvény megjelenítése mindig + Android 1080p Android 1080p + Angle Gradient Kúpos színátmenet + Angle Jitter Szög ingadozása + Angle: Szög: + Anti-alias Élsimítás + Anti-aliasing method Élsimítási mód + Apply Per Tip Alkalmazás hegyenként + Apply the pen's tilt and rotation to the tip shape A toll dőlését és elforgatását alkalmazza a hegy alakjára + Artboard Rajztábla + Artboard Background Color Rajztábla háttérszíne + Artboard from Group Rajztábla csoportból + Artboard from Layers Rajztábla rétegekből + Artboards Rajztáblák + At Itt + Auto Adjust Opacity Átlátszatlanság automatikus igazítása + Auto Center Preview Előnézet automatikus középre helyezése + Auto Show Overlay Fedvény megjelenítése automatikusan + Background Eraser Háttérradír + Baseline Alapvonal + Baseline shift Alapvonal-eltolás + Baseline Shift Alapvonal-eltolás + Basic Alapbeállítások + Before Előtte + Bend: Hajlás: + Bevel Levágott + Bicubic Bikubikus + Bilinear Bilineáris + Black Fekete + Black, White Fekete, fehér + Block Blokk + Blur Tool Elmosó + Bold Félkövér + Both Axes Mindkét tengely + Brightness Jitter Fényerő ingadozása + Bring the selected pin forward A kijelölt tű előrébb hozása + Brush Ecset + Brush Name Ecsetnév + Brush name Ecsetnév + Brush picker Ecsetválasztó + Brush Pose Ecsettartás + Brush Preset Options Ecset-előbeállítás beállításai + Brush preview color Ecset-előnézet színe + Brush Projection Ecsetvetítés + Brush Settings… (F5): every option of this brush Ecsetbeállítások… (F5): az ecset összes beállítása + Brush Stroke Ecsetvonás + Brush Tip Ecsethegy + Brush Tip Shape Ecsethegy alakja + Brush Tool Ecset + Build-up Felhalmozódás + Burn Égetés + Burn Tool Égető + Butt / Flat Tompa / lapos + Cache Level: Gyorsítótárszint: + Cancel any current edits (Esc) Folyamatban lévő módosítások elvetése (Esc) + Cancel current crop operation (Esc) Körbevágás elvetése (Esc) + Cancel transform Alakítás elvetése + Cancel transform (Esc) Alakítás elvetése (Esc) + Cancel warp (Esc) Hajlítás elvetése (Esc) + Canvas Vászon + Caps Vonalvégek + Catch-up on Stroke End Utolérés a vonás végén + Center Középen + Center text Szöveg középre igazítása + Centre the gradient (offset 0, 0) Színátmenet középre helyezése (eltolás: 0, 0) + Change Cserél + Change All Mindet cserél + Change To: Csere erre: + Character Karakter + Check All Layers Minden réteg ellenőrzése + Check Spelling Helyesírás-ellenőrzés + Clear Override Felülbírálás törlése + Clear Selection Kijelölés törlése + Click a red pupil to neutralize it Kattints egy vörös pupillára a semlegesítéséhez + Click to add a point, right-click to remove Kattintással pontot adhatsz hozzá, jobb kattintással eltávolíthatod + Click to apply, {key}-click to clear overrides, double-click for Style Options Kattintással alkalmazhatod, {key}+kattintással törölheted a felülbírálásokat, dupla kattintással megnyithatod a stílusbeállításokat + Click to edit the gradient Kattints a színátmenet szerkesztéséhez + Click to sample the foreground colour · {key}-click for background Kattintás: mintavétel az előtérszínhez · {key}+kattintás: a háttérszínhez + Clone source set at {x}, {y} Klónozási forrás beállítva: {x}, {y} + Clone Stamp Klónozó bélyegző + Close Style Options Stílusbeállítások bezárása + Color Szín + Color Dynamics Színdinamika + Color Picker (Crop Shield Color) Színválasztó (takarás színe) + Color Picker (Solid Color) Színválasztó (egyszínű kitöltés) + Color Picker (Stroke Color) Színválasztó (körvonalszín) + Color Picker (Text Color) Színválasztó (szövegszín) + Color Replacement Színcserélő + Color: Szín: + Commit any current edits ({key}) Folyamatban lévő módosítások véglegesítése ({key}) + Commit current crop operation ({key}) Körbevágás véglegesítése ({key}) + Commit Puppet Warp ({key}) Bábtorzítás véglegesítése ({key}) + Commit transform Alakítás véglegesítése + Commit transform ({key}) Alakítás véglegesítése ({key}) + Commit warp ({key}) Hajlítás véglegesítése ({key}) + Condensed Keskeny + Content-Aware Tartalomérzékeny + Content-Aware Move Tartalomérzékeny mozgatás + Contiguous Összefüggő + Control: Vezérlés: + Corners Sarkok + Could not complete the Free Transform command because the layer is locked. A szabad alakítás nem hajtható végre, mert a réteg zárolva van. + Could not transform: the layer is empty Az alakítás nem hajtható végre: a réteg üres + Could not use Puppet Warp: no opaque pixels A bábtorzítás nem használható: nincsenek átlátszatlan pixelek + Could not use Puppet Warp: the layer is empty A bábtorzítás nem használható: a réteg üres + Could not use the move tool because the layer is locked. A Mozgatás eszköz nem használható, mert a réteg zárolva van. + Count Jitter Darabszám ingadozása + Create a new elliptical frame Új ellipszis alakú keret létrehozása + Create a new rectangular frame Új téglalap alakú keret létrehozása + Create Ellipse Ellipszis létrehozása + Create new brush from the current settings Új ecset a jelenlegi beállításokból + Create new brush from these settings Új ecset ezekből a beállításokból + Create new brush preset from the current settings Új ecset-előbeállítás a jelenlegi beállításokból + Create new style (from the selected type) Új stílus létrehozása (a kijelölt szövegből) + Create Polygon Sokszög létrehozása + Create Rectangle Téglalap létrehozása + Create Texture Textúra létrehozása + Create Triangle Háromszög létrehozása + Crop Shield color Takarás színe + current jelenlegi + Current Jelenlegi + Current & Below Jelenlegi és alatta lévők + Current & Below No Adjustments Jelenlegi és alatta lévők, korrekciók nélkül + Current Layer Jelenlegi réteg + Custom Egyéni + Cycle Orientation Tájolás váltása + Cycle Overlay Fedvény váltása + Darken Amount Sötétítés mértéke + Dash Szakasz + Dash / gap lengths × stroke width Szakasz- és hézaghossz × vonalvastagság + Dash offset Szaggatás eltolása + Dashed Szaggatott + Dashed line Szaggatott vonal + Degrees Fok + Delete Brush Ecset törlése + Delete brush Ecset törlése + Delete Crop Preset… Körbevágási előbeállítás törlése… + Delete Group Csoport törlése + Delete style Stílus törlése + Density: Sűrűség: + Depth Jitter Mélység ingadozása + Destination Cél + Diagonal Átlós + Diameter Átmérő + Diameter: Átmérő: + Diamond Gradient Rombusz alakú színátmenet + Discontiguous Nem összefüggő + Divide… Felosztás… + Dodge Derítés + Dodge Tool Derítő + Dodge/Burn Derítés/égetés + Done Kész + Dot Pont + Dots Pontok + Dotted Pontozott + Drag a rectangle around the object Húzz egy téglalapot az objektum köré + Drag the arrow to set the angle, the dots to set the roundness A nyíl húzásával a szöget, a pontokéval a kerekséget állíthatod + Dual Axis Kettős tengely + Dual Brush Kettős ecset + Duplicate Kettőzés + Duplicate style Stílus kettőzése + Dynamics Dinamika + Enable Crop Shield Körbevágási takarás bekapcsolása + Enable {name} Bekapcsolás: {name} + Enhance Edge Élek javítása + Enter a name for the crop preset Add meg a körbevágási előbeállítás nevét + Entire Font Teljes betűtípus + Erase Radírozás + Eraser Radír + Expansion: Bővítés: + Exposure: Expozíció: + Extend Kiterjesztés + Extra Bold Extrakövér + Extra Light Extravékony + ExtraBold Extrakövér + ExtraLight Extravékony + Fade Halványulás + Fade steps Halványulás lépései + Faux Bold Álfélkövér + Faux Italic Áldőlt + Fewer Points Kevesebb pont + Fill Layer Kitöltőréteg + Fill new type layers with placeholder Új szövegrétegek kitöltése helyőrző szöveggel + Find Edges Élkeresés + Finger Painting Ujjfestés + First Line Indent Első sor behúzása + Fixed Rögzített + Flip Tükrözés + Flip X X-tükrözés + Flip X Jitter X-tükrözés ingadozása + Flip Y Y-tükrözés + Flip Y Jitter Y-tükrözés ingadozása + Flow Áramlás + Flow Jitter Áramlás ingadozása + Font Betűtípus + Font size Betűméret + Foreground to Background Előtérszínből háttérszínbe + Foreground/Background Jitter Előtér/háttér ingadozása + From Center Középpontból + Front Image Elülső kép + Gap Hézag + General Általános + Glyph Glifa + Golden Ratio Aranymetszés + Golden Spiral Aranyspirál + Gradient Színátmenet + Gradient Editor Színátmenet-szerkesztő + Gradient Options Színátmenet beállításai + Gradient picker Színátmenet-választó + Gradient stops Színátmenetpontok + Grid view Rácsnézet + Grid: Rács: + Group name Csoport neve + H M + Hard Kemény + Hardness Keménység + Hardness: Keménység: + Healing Javítás + Healing Brush Javítóecset + Height Magasság + Height: Magasság: + Hide Auto Slices Automatikus szeletek elrejtése + History Brush Előzményecset + Horizontal scale Vízszintes méretarány + Horizontal type Vízszintes szöveg + Hue Jitter Színárnyalat ingadozása + Hyphenate Elválasztás + Ignore Kihagy + Ignore All Mindet kihagy + Import Brushes… Ecsetek importálása… + Import Brushes… (.abr) Ecsetek importálása… (.abr) + Impressionist Impresszionista + Indent First Első sor behúzása + Indent first line Első sor behúzása + Indent Left Bal behúzás + Indent left margin Behúzás balról + Indent Right Jobb behúzás + Indent right margin Behúzás jobbról + Initial Direction Kezdeti irány + Inside Belül + Interpolation: Interpoláció: + Italic Dőlt + Justification Sorkizárás + Justify Sorkizárt + Justify all Teljes sorkizárás + Justify last centered Sorkizárt, utolsó sor középen + Justify last left Sorkizárt, utolsó sor balra + Justify last right Sorkizárt, utolsó sor jobbra + Kerning Alávágás + Lasso around an area, then drag the selection Keríts körbe egy területet lasszóval, majd húzd el a kijelölést + Leading Sorköz + Leading (set to the font size × auto-leading when Auto) Sorköz (automatikus értéknél: betűméret × automatikus sorköz) + Left align text Szöveg balra igazítása + Letter Letter + Letter Spacing Betűköz + Library Könyvtár + Ligature Ligatúra + Light Vékony + Limits: Határok: + Linear Gradient Lineáris színátmenet + Linear Height Lineáris magasság + Lines Vonalak + Link radii Sugarak összekapcsolása + List view Listanézet + Load Töltet + Loading fonts… Betűtípusok betöltése… + Location Hely + Location: Hely: + Lock: keep these settings when picking another preset Zárolva: ezek a beállítások megmaradnak, ha másik előbeállítást választasz + Loop Ismétlődés + Lorem Ipsum Lorem ipsum + Maintain aspect ratio Oldalarány megtartása + Make Landscape Fekvő tájolás + Make Portrait Álló tájolás + Make Work Path Munkaútvonal készítése + Match Canvas Vászonnal egyező + Mean: Átlag: + Median: Medián: + Medium Közepes + Metrics Metrikus + Minimum Depth Minimális mélység + Minimum Diameter Minimális átmérő + Minimum Roundness Minimális kerekség + Miter Hegyes + Miter limit Hegyességi korlát + Mix Keverés + Mix Jitter Keverés ingadozása + Mixer Brush Keverőecset + Mode: Mód: + More Points Több pont + Mouse Egér + Move Mozgatás + My Brushes Saját ecsetek + Name Név + Nearest Neighbor Legközelebbi szomszéd + Never Soha + Never Show Overlay Fedvény megjelenítése soha + New Brush Új ecset + New Brush Preset Új ecset-előbeállítás + New Brush Preset… Új ecset-előbeállítás… + New Crop Preset… Új körbevágási előbeállítás… + New Tool Preset Új eszköz-előbeállítás + No brush presets Nincs ecset-előbeállítás + No character style Nincs karakterstílus + No patterns Nincs mintázat + No subject found — try a rectangle around the object Nem található fő téma – próbálj egy téglalapot húzni az objektum köré + No subject found — try Object Selection or the Lasso Nem található fő téma – próbáld az Objektumkijelölést vagy a Lasszót + Noise Zaj + Not in Dictionary: Nincs a szótárban: + Now drag the selection onto another area Most húzd a kijelölést egy másik területre + Object Selection found nothing there — try a tighter rectangle Az Objektumkijelölés ott nem talált semmit – próbálj szűkebb téglalapot + Off Ki + Off: the speed of the pointer sets the spacing Kikapcsolva: a térközt a mutató sebessége határozza meg + Opacity Jitter Átlátszatlanság ingadozása + Opacity: Átlátszatlanság: + Open paths use centered strokes Nyitott útvonalon a körvonal középre igazított + Optical Optikai + Option-click to define a source point to clone from Option+kattintással add meg a klónozás forráspontját + Outside Kívül + Override Pressure Nyomás felülbírálása + Override Rotation Elforgatás felülbírálása + Override Tilt Dőlés felülbírálása + Paints from the document's opening state A dokumentum megnyitáskori állapotából fest + Paper Papír + Paragraph Bekezdés + Patch Foltozó + Patch: Foltozás: + Pattern Mintázat + Pattern Size Mintázatméret + Pattern Stamp Mintázatbélyegző + Pen Toll + Pen Pressure Tollnyomás + Pen Tilt Tolldőlés + Pencil Ceruza + Perspective Crop Perspektivikus körbevágás + Pin Depth: Tűmélység: + Pixels: Pixelek: + Point Sample Pontminta + Pressure curve Nyomásgörbe + Preview Image Előnézeti kép + Pro Pro + Promote Előléptetés + Protect Detail Részletek védelme + Protect Foreground Color Előtérszín védelme + Protect Texture Textúra védelme + Proximity Match Közelségi egyezés + Pulled String Mode Húzott zsinór mód + Punctuation Írásjelek + Pupil Size Pupillaméret + Puppet Warp is not active A bábtorzítás nem aktív + Purity Színtisztaság + Quick Selection found nothing there A Gyorskijelölés ott nem talált semmit + Radial Gradient Körkörös színátmenet + Radius: Sugár: + Ratio Arány + Raw Data Nyers adatok + Recently Used Legutóbb használt + Redefine style by current selection Stílus újbóli megadása a jelenlegi kijelölésből + Reflected Gradient Tükrözött színátmenet + Regular Normál + Remove Eltávolítás + Remove All Pins Összes tű eltávolítása + Remove pair Pár eltávolítása + Rename Brush Ecset átnevezése + Rename Brush… Ecset átnevezése… + Rename Group Csoport átnevezése + Rename Group… Csoport átnevezése… + Reset Visszaállít + Reset Alignment Igazítás visszaállítása + Reset all tools to their default settings? Visszaállítod az összes eszközt az alapértelmezett beállításokra? + Reset the brush to the defaults Ecset visszaállítása az alapértékekre + Restore original size Eredeti méret visszaállítása + Right align text Szöveg jobbra igazítása + Right click with painting tools Jobb kattintás festőeszközökkel + Rigid Merev + Roman Álló + Rotate: Forgatás: + Rotation Elforgatás + Round Lekerekített + Roundness Jitter Kerekség ingadozása + Roundness: Kerekség: + Rule of Thirds Harmadolási szabály + Sample All Layers Mintavétel minden rétegből + Sample Size: Mintaméret: + Sample: Minta: + Sampling: Background Swatch Mintavétel: háttérszín + Sampling: Continuous Mintavétel: folyamatos + Sampling: Once Mintavétel: egyszeri + Saturate Telítés + Saturation Jitter Telítettség ingadozása + Save As Preset Mentés előbeállításként + Scale: Méretarány: + Scatter Szóródás + Scattering Szóródás + Search Brushes Ecsetek keresése + Search fonts Betűtípusok keresése + Select a type layer or click into text to insert glyphs Glifák beszúrásához jelölj ki egy szövegréteget, vagy kattints a szövegbe + Select a type layer to edit its character and paragraph settings. Jelölj ki egy szövegréteget a karakter- és bekezdésbeállítások szerkesztéséhez. + Select text to apply a character style Karakterstílus alkalmazásához jelölj ki szöveget + Select the crop presets to delete Jelöld ki a törlendő körbevágási előbeállításokat + Select the crop presets to delete: Jelöld ki a törlendő körbevágási előbeállításokat: + Semi Bold Középkövér + SemiBold Középkövér + Send the selected pin backward A kijelölt tű hátrébb küldése + Set additional Crop options További körbevágási beállítások + Set painting symmetry options Festési szimmetria beállításai + Set the overlay options for the Crop tool A Körbevágás eszköz fedvénybeállításai + Set the text color Szövegszín beállítása + Settings: Beállítások: + Shape Dynamics Alakdinamika + Sharpen Tool Élesítő + Show bounding box when dragging layer Határolókeret megjelenítése réteg húzásakor + Show Cropped Area Levágott terület megjelenítése + Show Grid Rács megjelenítése + Show highlight on rollover Kiemelés rámutatáskor + Show Sampling Ring Mintavételi gyűrű megjelenítése + Show slice numbers Szeletszámok megjelenítése + Show: Megjelenítés: + Sides: Oldalak: + Size Méret + Size Jitter Méret ingadozása + Slant Dőlésszög + Slice color Szeletszín + Slice Options Szeletbeállítások + Slice Options… Szeletbeállítások… + Slices are locked (View › Lock Slices) A szeletek zárolva vannak (Nézet › Szeletek zárolása) + Slices From Guides Szeletek segédvonalakból + Small Caps Kiskapitális + Smart quotes Tipográfiai idézőjelek + Smoothing Simítás + Smudge Maszatoló + Smudge Tool Maszatoló + Snap to Origin Illesztés az origóhoz + Soft Lágy + Solid Folytonos + Solid Color Egyszínű + Space After Térköz utána + Space Before Térköz előtte + Spacing Térköz + Sparkle Csillogás + Spell check complete. A helyesírás-ellenőrzés befejeződött. + Split: Felosztás: + Sponge Tool Szivacs + Spot Healing Brush Foltjavító ecset + Stamp Down Másolat egyesítése lefelé + Stamp Visible Láthatók egyesített másolata + Star Ratio: Csillagarány: + Std Dev: Szórás: + Straighten the image by drawing a line on it Húzz egy vonalat a képen a kiegyenesítéshez + Strength: Erősség: + Strikethrough Áthúzás + Stroke Catch-up Vonás utolérése + Stroke color Körvonalszín + Stroke Color Körvonalszín + Stroke Options Körvonal beállításai + Structure: Struktúra: + Style Options Stílusbeállítások + Style Options… Stílusbeállítások… + Style: Normal Stílus: normál + Stylus Wheel Tollkerék + Suggestions: Javaslatok: + Swaps height and width Felcseréli a magasságot és a szélességet + Switch between free transform and warp modes Váltás a szabad alakítás és a hajlítás mód között + Symbols Szimbólumok + Symmetry Off Szimmetria ki + Text color Szövegszín + Text engine Szövegmotor + Text Layer Szövegréteg + Texture Each Tip Textúra hegyenként + The gradient the Gradient tool paints with A Színátmenet eszközzel festett színátmenet + There is no crop size or ratio to save Nincs menthető körbevágási méret vagy arány + Thin Hajszálvékony + Tilt Scale Dőlés méretaránya + Tilt X Dőlés X + Tilt Y Dőlés Y + Time Idő + Toggle text orientation Szövegirány váltása + Toggle the Brush Settings panel Ecsetbeállítások panel megjelenítése/elrejtése + Toggle the Brush Settings panel (F5) Ecsetbeállítások panel megjelenítése/elrejtése (F5) + Tolerance: Tűrés: + Tool Options Eszközbeállítások + Tracking Betűköz + Tracking (1/1000 em) Betűköz (1/1000 em) + Transfer Átvitel + Transformation Alakítás + Transparent Átlátszó + Triangle Háromszög + Type Options Szövegbeállítások + Type Styles Szövegstílusok + Type units Szöveg mértékegysége + Type: Típus: + Underline Aláhúzás + Unlock Feloldás + Unlock: picking a preset replaces these settings Feloldva: előbeállítás választásakor ezek a beállítások lecserélődnek + Update the current brush with these settings A jelenlegi ecset frissítése ezekkel a beállításokkal + Updates Frissítések + Use Classic Mode Klasszikus mód használata + Use esc to commit Véglegesítés az Esc billentyűvel + Use finite lengths and a positive pattern total Véges hosszakat adj meg, és a mintázat teljes hossza legyen pozitív + Use legacy free transform Régi szabad alakítás használata + Vary round brush hardness on hud Kerek ecset keménységének állítása a HUD-on + Version Verzió + Vertical Függőleges + Vertical scale Függőleges méretarány + Vertical type Függőleges szöveg + W Sz + W x H x Resolution Sz x M x felbontás + Warp Text Szövegtorzítás + Warp: Hajlítás: + Weight Vastagság + Wet Nedvesség + Wet Edges Nedves szélek + Wetness Jitter Nedvesség ingadozása + Width Szélesség + Width: Szélesség: + X X + {key} to subtract {key}: kivonás + {key}-click to set the source {key}+kattintással állíthatod be a forrást + {label} is not available: {reason} Nem érhető el: {label} ({reason}) +fontWeight Black Ultrakövér +fontWeight Bold Félkövér +fontWeight ExtraBold Extrakövér +fontWeight ExtraLight Extravékony +fontWeight Italic Dőlt +fontWeight Light Vékony +fontWeight Medium Közepes +fontWeight Regular Normál +fontWeight SemiBold Középkövér +fontWeight Thin Hajszálvékony + +# Layers, masks, layer styles and blend modes + 30 fps 30 fps + Add Layer Above to Selection Felette lévő réteg hozzáadása a kijelöléshez + Add Layer Below to Selection Alatta lévő réteg hozzáadása a kijelöléshez + Adjustment Korrekció + Advanced Blending Speciális keverés + Align and Distribute Igazítás és elosztás + Align {what} Igazítás: {what} + Alpha Alfa + Altitude Magasság + Angle Szög + Anti-aliased Élsimítás + Appearance (Layer Style) Megjelenés (rétegstílus) + Apply Image Kép alkalmazása + Apply Mask Maszk alkalmazása + Apply next selected layer comp Következő kijelölt rétegkompozíció alkalmazása + Apply previous selected layer comp Előző kijelölt rétegkompozíció alkalmazása + Apply {option}: {state} Alkalmazás – {option}: {state} + Bevel & Emboss Fazetta és domborítás + Black & White Fekete-fehér + Blend Clipped Layers as Group Vágott rétegek keverése csoportként + Blend If: Keverés, ha: + Blend Interior Effects as Group Belső effektusok keverése csoportként + Blend Mode Keverési mód + Blending Options Keverési beállítások + Blue Kék + Bold Italic Félkövér dőlt + Calculations Számítások + Capture the visibility, position and style of every layer with + below, then switch between versions. Az alábbi + gombbal rögzítheted minden réteg láthatóságát, pozícióját és stílusát, majd válthatsz a változatok között. + Channel Csatorna + Channels: Csatornák: + Choke Zsugorítás + Clear Brush Strokes Ecsetvonások törlése + CMYK: CMYK: + Collapse Összecsukás + Collapse All Groups Összes csoport összecsukása + Collapse group ({key}-click: all groups) Csoport összecsukása ({key}+kattintás: minden csoport) + Color Burn Színégetés + Color Burn (Paint.NET) Színégetés (Paint.NET) + Color Decontaminate Színszennyeződés eltávolítása + Color Dodge Színderítés + Color Dodge (Paint.NET) Színderítés (Paint.NET) + Color indicates A szín jelöli + Color Indicates Masked Areas A szín a maszkolt területeket jelöli + Color Indicates Selected Areas A szín a kijelölt területeket jelöli + Color Overlay Színfedés + Color Picker (Layer Style Color) Színválasztó (rétegstílus színe) + Color Swatches Színminták + Cone Kúp + Cone (Inverted) Kúp (fordított) + Cone - Inverted Kúp – fordított + Confirm Megerősít + Contour Kontúr + Contour Editor Kontúrszerkesztő + Convert to Alpha Channel Átalakítás alfacsatornává + Corner Sarokpont + Cove - Deep Öböl – mély + Cove - Shallow Öböl – sekély + Create new channel Új csatorna létrehozása + Create new layer Új réteg létrehozása + Create new layer comp Új rétegkompozíció létrehozása + Darken Sötétítés + Darker Color Sötétebb szín + Decontaminate Szennyeződés eltávolítása + Deep Mély + Defringe Szegélyjavítás + Delete Channel Csatorna törlése + Delete current channel Aktuális csatorna törlése + Delete layer comp Rétegkompozíció törlése + Delete layer ‘{name}’? Törlöd ezt a réteget: „{name}”? + Delete layer? Törlöd a réteget? + Delete {count} layers? {names} {count} réteget törölsz? {names} + Depth Mélység + Diagonal (Descending) Átlós (csökkenő) + Diamond Rombusz alakú + Difference Különbség + Direction Irány + Dissolve Szétszórás + Distance Távolság + Divide Osztás + Domed Domború + Domed (Inverted) Domború (fordított) + Don't show again Ne jelenjen meg újra + Double click layer mask launches select and mask A rétegmaszkra duplán kattintva megnyílik a Kijelölés és maszk + Down Le + Drop Shadow Vetett árnyék + Drop shadow Vetett árnyék + Duplicate Channel Csatorna kettőzése + Duplicate Layer Réteg kettőzése + Duplicate Layers Rétegek kettőzése + Edge Szél + Exclusion Kizárás + Exit Quick Mask Kilépés a gyorsmaszkból + Expand Kibontás + Expand Detection Area Észlelési terület bővítése + Expand group ({key}-click: all groups) Csoport kibontása ({key}+kattintás: minden csoport) + F cycles the views; hold X to see the image Az F lépteti a nézeteket; az X nyomva tartásával a kép látszik + File Format Fájlformátum + Fill Opacity Kitöltés átlátszatlansága + From Kezdőszín + Fuchsia Fukszia + Gaussian Gauss + General Blend Általános keverés + General Blending Általános keverés + Global Light Globális fény + Gloss Contour Csillogáskontúr + Glow Izzás + GPU backend GPU-háttérrendszer + Gradient Overlay Színátmenet-fedés + Gray Szürke + Green Zöld + Group Csoport + Half Round Félkör + Hard Light Kemény fény + Hard Mix Kemény keverés + Hide the layer's effects ({key}-click: all layers) Réteg effektusainak elrejtése ({key}+kattintás: minden réteg) + Horizontally Vízszintesen + Indigo Indigó + Information Információ + Inner Bevel Belső fazetta + Inner Glow Belső ragyogás + Inner Shadow Belső árnyék + Invalid delete command Érvénytelen törlési parancs + Invert Invertálás + Knockout Kiütés + Lab Lab + Last Document State Dokumentum utolsó állapota + Layer deleted, but confirmation preference could not be saved: {error} A réteg törölve, de a megerősítési beállítás nem menthető: {error} + Layer Knocks Out Drop Shadow A réteg kiüti a vetett árnyékot + layer mask rétegmaszk + Layer Mask Hides Effects A rétegmaszk elrejti az effektusokat + Lighten Világosítás + Lighter Color Világosabb szín + Linear Lineáris + Linear Burn Lineáris égetés + Linear Dodge Lineáris derítés + Linear Dodge (Add) Lineáris derítés (összeadás) + Linear Light Lineáris fény + Link Összekapcsolás + Link the mask to the layer Maszk összekapcsolása a réteggel + Link width and height Szélesség és magasság összekapcsolása + Link with Layer Összekapcsolás a réteggel + Load channel as selection Csatorna betöltése kijelölésként + Load Selection Kijelölés betöltése + Load… Betöltés… + Lock Layers Rétegek zárolása + Luminosity Fényesség + Magenta Magenta + Make Default Legyen alapértelmezett + Marching Ants Menetelő hangyák + Masked Areas Maszkolt területek + Masks Maszkok + Merge Channels… Csatornák egyesítése… + Merge Spot Channel Direktszín-csatorna egyesítése + Method Módszer + Minimum Minimum + Missing delete command Hiányzó törlési parancs + Mode Mód + Multiply Szorzás + Negation Negálás + New Channel Új csatorna + New Channel… Új csatorna… + New Spot Channel… Új direktszín-csatorna… + New Style… Új stílus… + Next Tovább + No Color Nincs szín + No document Nincs dokumentum + No Image Nincs kép + No selection Nincs kijelölés + Normal Normál + off ki + On Black Feketén + On Layers Rétegeken + On White Fehéren + Onion Skin Hagymahéj + Opacity Átlátszatlanság + Open Folder Mappa megnyitása + Operation Művelet + Outer Bevel Külső fazetta + Outer Glow Külső ragyogás + Overlay Átfedés + Overlay Color Átfedés színe + Overlay Opacity Átfedés átlátszatlansága + Paint over hair or fur on the image; Alt paints the other mode Fesd át a hajat vagy a szőrzetet a képen; Alt lenyomásával a másik módban festesz + Pass Through Áteresztés + Pattern Overlay Mintázatfedés + Pillow Emboss Párnás domborítás + Pin Light Pontfény + Pixel Pixel + Position Pozíció + Precise Pontos + Preserve Transparency Átlátszóság megőrzése + Quick Mask Gyorsmaszk + Radial Körkörös + Red Vörös + Refine Finomítás + Refine Edge Brush (R) Élfinomító ecset (R) + Refine Strokes Finomító ecsetvonások + Reflect Visszaverődés + Reflected Tükrözött + Rename Channel… Csatorna átnevezése… + Reorder Layer Réteg átrendezése + Reset to Default Visszaállítás alapértelmezettre + Restore Original Edge Eredeti él visszaállítása + Reverse Megfordítás + RGB: RGB: + Ring Gyűrű + Ring - Double Gyűrű – dupla + Rolling Slope - Descending Hullámos lejtő – csökkenő + Rounded Steps Lekerekített lépcsők + Satin Szatén + Save Selection Kijelölés mentése + Save selection as channel Kijelölés mentése csatornaként + Save… Mentés… + Sawtooth 1 Fűrészfog 1 + Scale Effects Effektusok méretezése + Screen Képernyő + Seafoam Tengerzöld + Select All Layers Összes réteg kijelölése + Select Bottom Layer Legalsó réteg kijelölése + Select Layer Above Felette lévő réteg kijelölése + Select Layer Below Alatta lévő réteg kijelölése + Select Top Layer Legfelső réteg kijelölése + Selected Areas Kijelölt területek + Shadow Árnyék + Shallow Sekély + Show Edge Él megjelenítése + Show Original Eredeti megjelenítése + Show the layer mask as an overlay Rétegmaszk megjelenítése átfedésként + Show the layer's effects ({key}-click: all layers) Réteg effektusainak megjelenítése ({key}+kattintás: minden réteg) + Skip Kihagy + Smart Filters Intelligens szűrők + Soft Light Lágy fény + Soften Lágyítás + Source Forrás + Split Channels Csatornák szétválasztása + Spot Direktszín + Spot Channel Direktszín-csatorna + Spot Color Direktszín + Spread Kiterjedés + Stroke Körvonal + Style {name} saved Stílus mentve: {name} + Subtract Kivonás + The document is closed A dokumentum be van zárva + The document or selected layers changed. Please request deletion again. A dokumentum vagy a kijelölt rétegek megváltoztak. Kérd újra a törlést. + This Layer: Ez a réteg: + To Végszín + Toggle visibility Láthatóság váltása + Transparency Shapes Layer Az átlátszóság alakítja a réteget + UI scale Felület méretezése + Underlying Layer: Alatta lévő réteg: + Unlink Szétkapcsolás + Unlink the mask from the layer Maszk szétkapcsolása a rétegtől + Up Fel + Update layer comp (visibility, position and appearance) Rétegkompozíció frissítése (láthatóság, pozíció és megjelenés) + Use Global Light Globális fény használata + vector mask vektormaszk + Vector Mask Hides Effects A vektormaszk elrejti az effektusokat + Vertically Függőlegesen + Visibility Láthatóság + Vivid Light Élénk fény + XOR XOR + Yellow Sárga + {verb} {mask} {layer} {verb}: {mask} ({layer}) +layerLabel No Color Nincs szín +layerLabel Red Piros +layerLabel Magenta Bíbor + Use Previous Layer to Create Clipping Mask Vágómaszk létrehozása az előző réteggel + Fill with {mode}-neutral color (white) Kitöltés {mode} módhoz semleges színnel (fehér) + Fill with {mode}-neutral color (black) Kitöltés {mode} módhoz semleges színnel (fekete) + Fill with {mode}-neutral color (50% gray) Kitöltés {mode} módhoz semleges színnel (50%-os szürke) + (No neutral color exists for {mode} mode.) ({mode} módhoz nincs semleges szín.) + +# Panels + (none) (nincs) + + Add variable + Változó hozzáadása + 30 frames @ 30 fps 30 képkocka, 30 fps + 32-bit Preview Options 32 bites előnézet beállításai + 50% Gray 50%-os szürke + 80% 80% + A save to {name} is still running; try again when it finishes Még tart egy mentés ide: {name}; próbáld újra, ha befejeződött + Add a layer style Rétegstílus hozzáadása + Add a mask (from the selection; {key} inverts) Maszk hozzáadása (a kijelölésből; {key}: invertálás) + Add an adjustment Korrekció hozzáadása + Add to selection ({key}) Hozzáadás a kijelöléshez ({key}) + Add vector mask Vektormaszk hozzáadása + Added swatch {name} Színminta hozzáadva: {name} + Adjustment visible Korrekció látható + Align and distribute Igazítás és elosztás + Align horizontal centers Igazítás a vízszintes középvonalakhoz + Align left edges Igazítás a bal szélekhez + Align right edges Igazítás a jobb szélekhez + Always use pressure for opacity Mindig nyomásfüggő átlátszatlanság + Always use pressure for size Mindig nyomásfüggő méret + Appearance Mode Megjelenési mód + Auto Erase Automatikus radírozás + Auto Hide Automatikus elrejtés + Auto-Select: Automatikus kijelölés: + Beep when done Hangjelzés befejezéskor + Brush Preset picker Ecset-előbeállítás-választó + Capture Rögzítés + Change Screen Mode (F) Képernyőmód váltása (F) + Click the first point or press {key} to close · Esc cancels Lezárás: kattintás az első pontra vagy {key} · Esc: megszakítás + Click the first point or press {key} to close · {del} removes a point · Esc cancels Lezárás: kattintás az első pontra vagy {key} · {del}: pont eltávolítása · Esc: megszakítás + Click to fill similar colours Kattintással kitöltheted a hasonló színeket + Click to zoom in · {key}-click to zoom out · drag right/left to zoom in/out Kattintás: nagyítás · {key}+kattintás: kicsinyítés · húzás jobbra/balra: nagyítás/kicsinyítés + Click, then move along an edge · click to fasten a point · {alt}-click for a straight segment · double-click to close Kattints, majd haladj végig egy élen · kattintás: pont rögzítése · {alt}+kattintás: egyenes szakasz · dupla kattintás: lezárás + Clip to layer below Vágás az alatta lévő réteghez + Clipped Vágott + Clone source {n} ({key}-click with Clone Stamp to set) Klónozási forrás: {n} (beállítás: {key}+kattintás a klónozó bélyegzővel) + Close Tab Group Lapcsoport bezárása + Collapse Panel Group Panelcsoport összecsukása + Color & Swatches Szín és színminták + Conform Nyújtás + Create a new group Új csoport létrehozása + Create a new layer Új réteg létrehozása + Create new document from current state Új dokumentum az aktuális állapotból + Create new fill or adjustment layer Új kitöltő- vagy korrekciós réteg létrehozása + Create new gradient Új színátmenet létrehozása + Create new group Új csoport létrehozása + Create new pattern from the selection Új mintázat létrehozása a kijelölésből + Create new shape from the current path Új alakzat létrehozása az aktuális útvonalból + Create new snapshot Új pillanatkép létrehozása + Create new style from the selected layer Új stílus létrehozása a kijelölt rétegből + Create new swatch Új színminta létrehozása + Create new tool preset Új eszköz-előbeállítás létrehozása + Create Video Timeline Videó-idővonal létrehozása + Create Warped Text Torzított szöveg létrehozása + Current Tool Only Csak az aktuális eszköz + Curves Görbék + Dark Sötét + Data Set: {name} ({index}/{total}) Adatkészlet: {name} ({index}/{total}) + Data Sets Adatkészletek + Default colours (D) Alapértelmezett színek (D) + Define Megadás + Define at least one variable first. Előbb adj meg legalább egy változót. + Delete Cropped Pixels Levágott pixelek törlése + Delete current state Aktuális állapot törlése + Delete layer Réteg törlése + Delete Timeline Idővonal törlése + Delete Tool Preset Eszköz-előbeállítás törlése + Delete tool preset Eszköz-előbeállítás törlése + Delete Workspace Munkaterület törlése + Discord Discord + Doc: {w} × {h} px Dok.: {w} × {h} px + Drag a box or click four corners · drag the corners onto the edges of a skewed shape · {commit} commits · Esc cancels Húzz egy keretet, vagy kattints a négy sarokra · húzd a sarkokat a ferde alakzat éleire · {commit}: véglegesítés · Esc: megszakítás + Drag a crop box · drag inside to move · edges resize ({ratio} ratio, {centre} centre) · Space moves while drawing · arrows nudge · X swaps orientation · {commit} commits · Esc cancels Húzz egy körbevágási keretet · belül húzva mozgatod · az élekkel átméretezed ({ratio}: arány, {centre}: középről) · Space: mozgatás rajzolás közben · nyilak: léptetés · X: tájolás cseréje · {commit}: véglegesítés · Esc: megszakítás + Drag around the centre to rotate the view · Shift constrains to 15° A középpont körül húzva forgatod a nézetet · Shift: 15°-os lépések + Drag or click polygon points · hold Alt during lasso for straight segments · {add} add · {sub} before drawing subtracts Húzz, vagy kattints sokszögpontokat · lasszózás közben Alt: egyenes szakaszok · {add}: hozzáadás · {sub} rajzolás előtt: kivonás + Drag to draw a gradient Húzással rajzolhatsz színátmenetet + Drag to move the active layer Húzással mozgathatod az aktív réteget + Drag to pan · hold Space with any tool Húzással mozgathatod a nézetet · bármely eszköznél: Space nyomva tartása + Duration Időtartam + Edit in Quick Mask Mode (Q) Szerkesztés gyorsmaszk módban (Q) + Edit in Standard Mode (Q) Szerkesztés normál módban (Q) + Edit Toolbar Eszköztár szerkesztése + Edit Toolbar… Eszköztár szerkesztése… + Enable airbrush-style build-up effects Festékszóró-szerű felhalmozódás bekapcsolása + Esc to cancel Esc: megszakítás + Expand Panel Group Panelcsoport kibontása + Export Swatches for Exchange… Színminták exportálása cseréhez… + Export Swatches… Színminták exportálása… + Exposure and Gamma Expozíció és gamma + Factor: Tényező: + Fill: Kitöltés: + Filter for adjustment layers Szűrés korrekciós rétegekre + Filter for pixel layers Szűrés pixelrétegekre + Filter for shape layers Szűrés alakzatrétegekre + Filter for smart objects Szűrés intelligens objektumokra + Filter for type layers Szűrés szövegrétegekre + Fit Igazítás + Fit Screen Képernyőhöz igazítás + Fixed Ratio Rögzített arány + Fixed Size Rögzített méret + Folder Mappa + Follows only edges this close to the pointer ([ and ] change it) Csak a mutatóhoz ennyire közeli éleket követi (a [ és a ] módosítja) + Foreground Előtérszín + Frame {current} / {total} Képkocka: {current} / {total} + Frequency Gyakoriság + Gamma: Gamma: + gradients színátmenetek + Group with Csoportosítás ezzel + Group · {n} layer Csoport · {n} réteg + Heart Szív + Hide Properties Tulajdonságok elrejtése + Higher values follow only edges that contrast sharply with their surroundings Nagyobb értéknél csak a környezetüktől élesen elütő éleket követi + Highlight Compression Csúcsfény-tömörítés + Hold Alt while drawing for straight segments Rajzolás közben az Alt nyomva tartásával egyenes szakaszokat húzhatsz + How often fastening points are placed by themselves Milyen gyakran kerülnek le automatikusan rögzítőpontok + Hsb HSB + image file path képfájl elérési útja + Import Brushes Ecsetek importálása + Import Swatches… Színminták importálása… + Imported Importált + Importing {what} from {file}… {what} importálása. Forrás: {file}… + Intersect with Metszet ezzel + Intersect with selection ({key}) Metszet a kijelöléssel ({key}) + Invalid panel layout; restored the default dock Érvénytelen panelelrendezés; az alapértelmezett dokkolás visszaállítva + Join the ArtCraft Discord ({url}) Csatlakozz az ArtCraft Discord-szerveréhez ({url}) + Keyboard Shortcuts Billentyűparancsok + Kind Fajta + Lasso Lasszó + Link layers Rétegek összekapcsolása + Lock Zárolás + Lock all Összes zárolása + Lock image pixels Képpixelek zárolása + Lock layer Réteg zárolása + Lock position Pozíció zárolása + Lock transparent pixels Átlátszó pixelek zárolása + Lock: Zárolás: + Marquee Kijelölőkeret + Menus Menük + Move Group Down Csoport mozgatása lefelé + Move Group Up Csoport mozgatása felfelé + Move to Áthelyezés ide + Name: Név: + New selection Új kijelölés + New Swatch Group… Új színmintacsoport… + New Swatch… Új színminta… + New Workspace Új munkaterület + Next frame Következő képkocka + No custom shape "{name}": pick one in the options bar or Window › Shapes Nincs ilyen egyéni alakzat: „{name}”; válassz egyet a beállítósávon vagy az Ablak › Alakzatok panelen + No presets for the current tool. Az aktuális eszközhöz nincs előbeállítás. + No properties Nincsenek tulajdonságok + No tool presets. Nincs eszköz-előbeállítás. + Open a document to define variables. Változók megadásához nyiss meg egy dokumentumot. + Open a document to use the timeline. Az idővonal használatához nyiss meg egy dokumentumot. + Opening Megnyitás + Opening {name} Megnyitás: {name} + Opening {name}… Megnyitás: {name}… + Panel Panel + Panel locations are saved in the workspace. Keyboard shortcuts, menus and toolbar are optional. A panelek helye a munkaterülettel együtt mentődik. A billentyűparancsok, a menük és az eszköztár mentése választható. + Panel menu Panelmenü + Patterns Mintázatok + Pause Szünet + Pixel Replacement Pixelcsere + Play Lejátszás + Pressure for Opacity Nyomásfüggő átlátszatlanság + Pressure for Size Nyomásfüggő méret + Prevent auto-nesting in and out of Artboards and Frames Automatikus be- és kiágyazás tiltása rajztábláknál és kereteknél + Previous frame Előző képkocka + Quick Actions Gyorsműveletek + Rasterize Fill Content Kitöltés tartalmának raszterizálása + Rasterize Smart Object Intelligens objektum raszterizálása + Reading… Olvasás… + Remember workspace changes Munkaterület-módosítások megjegyzése + Remove Background Háttér eltávolítása + Remove variable Változó eltávolítása + Rename Tool Preset Eszköz-előbeállítás átnevezése + Rename Tool Preset… Eszköz-előbeállítás átnevezése… + Rename… Átnevezés… + Replace Swatches… Színminták cseréje… + Reset Swatches Színminták visszaállítása + Reset Transform Alakítás visszaállítása + Resize panel window Panelablak átméretezése + Resize panels Panelek átméretezése + Sample Size Mintaméret + Save File Fájl mentése + Save Pixel Aspect Ratio Pixelarány mentése + Saving {name} Mentés: {name} + Scrubby Zoom Húzásos nagyítás + Search commands Parancsok keresése + Search commands, tools and panels Parancsok, eszközök és panelek keresése + Search commands, tools and panels… Parancsok, eszközök és panelek keresése… + Select Subject Fő téma kijelölése + Set additional smoothing options További simítási beállítások + Set background color Háttérszín beállítása + Set foreground color Előtérszín beállítása + Shape: Alakzat: + Shift Shift + Show Extras Options Segédelemek beállításai + Show Overlay Átfedés megjelenítése + Show Transform Controls Alakítási vezérlők megjelenítése + Size: Méret: + Source: not set ({key}-click with Clone Stamp) Forrás: nincs beállítva ({key}+kattintás a klónozó bélyegzővel) + Source: set Forrás: beállítva + Style Stílus + Subtract from selection ({key}) Kivonás a kijelölésből ({key}) + Swap colours (X) Színek cseréje (X) + System title bar Rendszer címsora + Text Replacement Szövegcsere + Thumbnail size (smallest: list) Bélyegképméret (a legkisebb: lista) + Tolerance Tűrés + Toolbar Eszköztár + Type Layer Szövegréteg + Ungroup Csoport bontása + Use tablet pressure to change pen width A tablet nyomása módosítja a toll szélességét + Variables bind a layer's visibility, text or pixels to a named data slot. A változók egy réteg láthatóságát, szövegét vagy pixeleit egy elnevezett adatmezőhöz kötik. + Visible Látható + Warning Figyelmeztetés + Working… Feldolgozás… + Workspaces Munkaterületek + Zoom Nagyító + {label} is writing the file and can no longer be cancelled {label}: a fájl írása folyamatban, már nem szakítható meg + {message} (notes: {n}; {warning}) {message} (megjegyzések: {n}; {warning}) + {mode} Color · {bits} bit Színmód: {mode} · {bits} bit + {name} Properties Tulajdonságok: {name} + ↑↓ navigate {key} run esc close ↑↓ navigálás {key} futtatás esc bezárás +navigator Zoom Nagyítás mértéke + +# Adjustments and filters + 3D LUT File 3D LUT-fájl + 90% 90% + Absolute Abszolút + Accented Edges Kiemelt élek + Adaptive Wide Angle isn't open Az Adaptív nagy látószög nincs megnyitva + Add Noise Zaj hozzáadása + Add Stop Átmenetpont hozzáadása + Add to Sample Hozzáadás a mintához + Adjust Interval For Loops Térköz igazítása hurkokhoz + Adjustment Preview Korrekció előnézete + Amount Mérték + Amplitude Max Legnagyobb amplitúdó + Amplitude Min Legkisebb amplitúdó + Angled Strokes Ferde vonások + Arbitrary Tetszőleges + Around Center Középpont körül + Artistic Művészi + Author: Szerző: + Auto Automatikus + Automatically level near-horizontal line segments A közel vízszintes szakaszok automatikus vízszintesre állítása + Automatically straighten and level Automatikus függőlegesre és vízszintesre állítás + Automatically straighten near-vertical line segments A közel függőleges szakaszok automatikus függőlegesre állítása + Background Level Háttérszint + Bamboo Bambusz + Bas Relief Dombormű + Base Tree Type Fa alaptípusa + Berries Bogyók + Best Legjobb + Black Clip Fekete levágása + Black Intensity Fekete intenzitása + Black Matte Fekete háttér + Blacks Feketék + Blend If Keverés, ha + Blocks Tömbök + Blue Yellow Kék–sárga + Blues Kékek + Blur Angle Elmosás szöge + Border Thickness Szegély vastagsága + Both Mindkettő + Bottom Alul + Box Blur Dobozelmosás + Branches Color Ágak színe + Branches Height Ágak magassága + Branches Thickness Ágak vastagsága + Brick Tégla + Brightness Fényerő + Brightness/Contrast Fényerő/kontraszt + Bristle Detail Sörték részletessége + Brush Detail Ecset részletessége + Brush Reconstruct Options Ecsetes helyreállítás beállításai + Brush Size Ecsetméret + Brush Strokes Ecsetvonások + Brush Tool Options Ecset beállításai + Brush Type Ecsettípus + Burlap Zsákvászon + Camera Calibration Fényképezőgép-kalibráció + Cancel (Esc) Mégse (Esc) + Candle Light Gyertyaláng + Cell Size Cellaméret + Center X Középpont X + Center Y Középpont Y + Chalk & Charcoal Kréta és szén + Chalk Area Krétás terület + Channel Mixer Csatornakeverő + Channel: Csatorna: + Charcoal Szénrajz + Charcoal Area Szenes terület + Charcoal Thickness Szén vastagsága + Chrome Króm + Clear All Összes törlése + Clear Amount Tiszta terület mértéke + Clear Samples Minták törlése + Click to add a point · drag off or {key}-click or Delete to remove Kattintással pontot adhatsz hozzá · eltávolítás: lehúzás, {key}+kattintás vagy Delete + Click to place · Alt-click or drag off-canvas to remove Kattintással elhelyezheted · eltávolítás: Alt+kattintás vagy a vásznon kívülre húzás + Click with the Note tool to add a note. Jegyzet hozzáadásához kattints a Jegyzet eszközzel. + Clumped Csomós + Coarse Dots Durva pontok + Color Balance Színegyensúly + Color Halftone Színes féltónus + Color Lookup Színleképezés + Color Range preview Színtartomány előnézete + Colored Pencil Színes ceruza + Colorize Színezés + Colors Színek + Commit Perspective Warp ({key}) Perspektivikus torzítás véglegesítése ({key}) + Constant Állandó + Constraints ({n}) Kényszerek ({n}) + Continue Folytat + Contrast Kontraszt + Contrasty Kontrasztos + Conté Crayon Conté-kréta + Cooling L B B Hűtőszűrő (LBB) + Cooling80 Hűtőszűrő (80) + Cooling82 Hűtőszűrő (82) + Correction Korrekció + Could not use Perspective Warp: the layer is empty A perspektivikus torzítás nem használható: a réteg üres + Count: {n} Darabszám: {n} + Crack Brightness Repedések fényereje + Crack Depth Repedések mélysége + Crack Spacing Repedések térköze + Craquelure Krakelé + Create a new count group Új számlálócsoport létrehozása + Crop Factor Szűkítési tényező + Crosshatch Keresztsraffozás + Crystallize Kristályosítás + Curve Splits Görbe osztáspontjai + Custom Branch Color Egyéni ágszín + Cutout Papírkivágás + Cyan Cián + Cyan · Red Cián · Vörös + Cyans Ciánok + Daisies Margaréták + Dark Intensity Sötét intenzitása + Dark Rough Sötét, durva + Dark Strokes Sötét vonások + Darken Only Csak sötétítés + Darkness Sötétség + De-Interlace Váltott soros megjelenítés megszüntetése + Deep Blue Mélykék + Deep Emerald Mély smaragd + Deep Red Mélyvörös + Deep Yellow Mélysárga + Default Alapértelmezett + Default Leaves Alapértelmezett levelek + Definition Határozottság + Delete count group Számlálócsoport törlése + Delete effect layer Effektusréteg törlése + Delete note Jegyzet törlése + Delete selected measurements Kijelölt mérések törlése + Density Sűrűség + Depth Map Mélységtérkép + Deselect All Összes kijelölés megszüntetése + Diffuse Diffúzió + Diffuse Glow Szórt ragyogás + Direction Balance Irányegyensúly + Displace Elmozdítás + Display Text Szöveg megjelenítése + Distortion Torzítás + Dither Árnyalatkeverés + Double Line Kettős vonal + Drag on the image to add a constraint; Shift for horizontal/vertical; right-click to delete. Húzással kényszert adhatsz a képhez; Shift: vízszintes/függőleges; jobb kattintás: törlés. + Dry Brush Száraz ecset + Dust & Scratches Por és karcolások + Dynamic color sliders Dinamikus színcsúszkák + Edge Brightness Élek fényereje + Edge Fidelity Élhűség + Edge Intensity Élek intenzitása + Edge Only Csak élek + Edge Simplicity Élek egyszerűsége + Edge Thickness Élvastagság + Edge Width Élszélesség + Emboss Domborítás + Enlarged Nagyított + Export selected measurements (CSV) Kijelölt mérések exportálása (CSV) + Exposure Expozíció + Extrude Kihúzás + Eyedropper Pipetta + Eyedroppers: Pipetták: + Fast Gyors + Fiber Length Szálhossz + Fibers Szálak + Field Blur Mezőelmosás + Film Grain Filmszemcse + Filter Gallery Szűrőgaléria + Filter: Szűrő: + Fine Dots Finom pontok + Fisheye Halszem + Fit in View Igazítás a nézethez + Flame Bottom Alignment Lángalap igazítása + Flame Lines Lángvonalak + Flame Shape Lángalak + Flame Style Lángstílus + Flame Type Lángtípus + Flat Shading Lapos árnyalás + Flower Color Virágszín + Focal Distance Fókusztávolság + Focal Length Gyújtótávolság + Font Size: Betűméret: + Foreground Level Előtérszint + Free Szabad + Freeze Lasso: drag to freeze an area, Alt-drag to thaw it (L) Fagyasztólasszó: húzással fagyaszthatsz egy területet, Alt+húzással feloldhatod (L) + Fresco Freskó + From Right Jobbról + Frosted Mattüveg + Full Spherical Teljes gömb + Fuzziness Elmosódottság + Fuzziness: Elmosódottság: + Gamma Gamma + Gamma Correction Gammakorrekció + Gaussian Blur Gauss-elmosás + Glass Üveg + Glow Amount Ragyogás mértéke + Glow Brightness Ragyogás fényereje + Glow Color Ragyogás színe + Glow Size Ragyogás mérete + Glowing Edges Izzó élek + Gradient Map Színátmenet-leképezés + Grain Szemcse + Grain Type Szemcsetípus + Graininess Szemcsésség + Grainy Dots Szemcsés pontok + Graphic Pen Grafikus toll + Greens Zöldek + Grid size Rácsméret + Grout Width Fugaszélesség + Halftone Pattern Féltónusmintázat + HDR Toning HDR-tónusozás + Hearts Szívek + High Pass Felüláteresztő + Highlight Csúcsfény + Highlight Amount Csúcsfény mértéke + Highlight Area Csúcsfény-terület + Highlight Radius Csúcsfény sugara + Highlight Strength Csúcsfény erőssége + Highlight Tone Csúcsfény tónusa + Highlights Csúcsfények + Highlights from: Csúcsfények ettől: + Horizontal Only Csak vízszintes + Hue Színárnyalat + Hue/Saturation Színárnyalat/telítettség + Image Balance Képegyensúly + In Black Bemeneti fekete + In White Bemeneti fehér + Ink Outlines Tintakörvonalak + Input: Bemenet: + Intensity Intenzitás + Invert All Összes invertálása + Invert has no settings. Az Invertálásnak nincsenek beállításai. + Invert Mask Maszk invertálása + Iris Blur Íriszelmosás + ISO ISO + Ivy Borostyán + Label Size Címkeméret + Layout Elrendezés + Leaf Color Levélszín + Leaves Amount Levelek mennyisége + Leaves Color Levelek színe + Leaves Size Levélméret + Left Diagonal Balra átlós + Legacy Régi + Length: Hossz: + Lens Blur Objektívelmosás + Lens Flare Lencsecsillanás + Less Kevesebb + Level Szint + Level Based Szintalapú + Levels Szintek + Light Dark Balance Világos–sötét egyensúly + Light Direction Fény iránya + Light Intensity Fényintenzitás + Light Rough Világos, durva + Light Source Fényforrás + Lighten Grout Fuga világosítása + Lighten Only Csak világosítás + Lighting Effects Fényhatások + Lightness Világosság + Liquify is not open A Képlékenyítés nincs megnyitva + Load 3D LUT 3D LUT betöltése + Load 3D LUT… 3D LUT betöltése… + Localized Color Clusters Helyi színcsoportok + Logical Length: Logikai hossz: + Logical Units: Logikai egységek: + Long Lines Hosszú vonalak + Long Strokes Hosszú vonások + Lut LUT + Mac OS Mac OS + Magenta · Green Magenta · Zöld + Magentas Magenták + Map Document Térkép-dokumentum + Marker Size Jelölőméret + Mask All Összes maszkolása + Mask Options Maszkbeállítások + Master Összes szín + Match Color Színillesztés + Max Offset Legnagyobb eltolás + Max Radius Legnagyobb sugár + Measure Mérés + Measurement Scale Mérési méretarány + Measurement Scale Marker Méretarány-jelölő + Medium Dots Közepes pontok + Medium Lines Közepes vonalak + Medium Strokes Közepes vonások + Mesh Size Hálóméret + Mezzotint Mezzotinto + Midtone Középtónus + Midtones Középtónusok + Midtones from: Középtónusok ettől: + Midtones to: Középtónusok eddig: + Monochrome Monokróm + More Több + Mosaic Mozaik + Mosaic Tiles Mozaiklapok + Motion Blur Mozgási elmosás + Move effect layer down Effektusréteg mozgatása lefelé + Move effect layer up Effektusréteg mozgatása felfelé + Movie Prime Filmes fix objektív + Multiple Flames Along Path Több láng az útvonal mentén + Multiple Flames One Direction Több láng egy irányba + Multiple Flames Path Directions Több láng az útvonal irányaiba + Multiple Flames Various Length Több láng eltérő hosszal + Neon Glow Neonragyogás + Neutralize Semlegesítés + Neutrals Semlegesek + New effect layer Új effektusréteg + Next note Következő jegyzet + No author Nincs szerző + No document open Nincs megnyitott dokumentum + None Nincs + Note Paper Jegyzetpapír + Number Of Levels Szintek száma + Ocean Ripple Óceánfodrozódás + Offset Eltolás + Oil Paint Olajfestmény + One Flame Along Path Egy láng az útvonal mentén + Opacity Threshold Átlátszatlansági küszöb + Open Megnyitás + Open a document to select a colour range. Színtartomány kijelöléséhez nyiss meg egy dokumentumot. + Orange Narancs + Out Black Kimeneti fekete + Out From Center Középponttól kifelé + Out Of Gamut Színtartományon kívüli + Out White Kimeneti fehér + Output Channel: Kimeneti csatorna: + Output Levels Kimeneti szintek + Output Levels: Kimeneti szintek: + Output: Kimenet: + Oval Ovális + Overlay Edge Élátfedés + Paint Daubs Festékpacák + Palette Knife Festőkés + Paper Brightness Papír fényereje + Parallel Párhuzamos + Patchwork Foltvarrás + Path Blur Útvonal-elmosás + Pattern Type Mintázattípus + Pencil Width Ceruzaszélesség + Perspective Warp is not active A perspektivikus torzítás nem aktív + Perspective Warp: draw planes along the image's perspective, then switch to Warp Perspektivikus torzítás: rajzolj síkokat a kép perspektívája mentén, majd válts Hajlítás módra + Photo Filter Fotószűrő + Photocopy Fénymásolat + Picture Frame Képkeret + Pinch Becsípés + Pixel Length: Pixelhossz: + Plaster Gipsz + Plastic Wrap Műanyag fólia + Point Curve Pontgörbe + Point Curve Blue Pontgörbe, kék + Point Curve Green Pontgörbe, zöld + Point Curve Red Pontgörbe, vörös + Pointed Hegyes + Pointillize Pontozás + Polar Coordinates Polárkoordináták + Polar To Rectangular Polárisból derékszögűbe + Pond Ripples Tavi fodrozódás + Poster Edges Poszterélek + Posterization Poszterizáció + Posterize Poszterizálás + Preserve Luminosity Fényesség megőrzése + Preset: Előbeállítás: + Presets: Előbeállítások: + Pressure Nyomás + Preview Előnézet + Previous note Előző jegyzet + Pro photo ProPhoto + Processing… Feldolgozás… + ProPhoto ProPhoto + ProPhoto RGB ProPhoto RGB + Protect Tones Tónusok védelme + Radial Blur Sugárirányú elmosás + Radius X X sugár + Radius Y Y sugár + Randomize Length Véletlenszerű hossz + Randomize Shapes Véletlenszerű alakok + Range: Tartomány: + Rate Ütem + Ready Készen áll + Reconstruct Helyreállítás + Rectangular To Polar Derékszögűből polárisba + Red Cyan Vörös–cián + Reds Vörösek + Reduce Color Noise Színzaj csökkentése + Reduce Noise Zajcsökkentés + Relative Relatív + Relief Domborulat + Remove Jpeg Artifact JPEG-hibák eltávolítása + Remove Planes Síkok eltávolítása + Remove stop Átmenetpont eltávolítása + Replace Color Színcsere + Reset to defaults Alapértékek visszaállítása + Restore All Összes visszaállítása + Resume Folytatás + Reticulation Retikuláció + RGB RGB + Right Diagonal Jobbra átlós + Ripple Fodrozódás + Ripple Magnitude Fodrozódás nagysága + Ripple Size Fodrozódás mérete + Rope Kötél + Roses Rózsák + Rough Pastels Durva pasztell + Ruler Vonalzó + Sample: foreground colour Minta: előtérszín + Sampled Colors Mintavételezett színek + Sandstone Homokkő + Saturation Telítettség + Scaling Méretezés + Scallops Csipkés szél + Select and Mask Kijelölés és maszk + Select Data Points Adatpontok kiválasztása + Select: Kijelölés: + Selection Kijelölés + Selection Preview: Kijelölés előnézete: + Selections Kijelölések + Selective Color Szelektív szín + Sepia Szépia + Set Black Point Feketepont beállítása + Set Neutral Gray Point Semleges szürkepont beállítása + Set White Point Fehérpont beállítása + Shadow Amount Árnyék mértéke + Shadow Intensity Árnyék intenzitása + Shadow Radius Árnyék sugara + Shadow Tone Árnyék tónusa + Shadows Árnyékok + Shadows up to: Árnyékok eddig: + Shadows/Highlights Árnyékok/csúcsfények + Shape Blur Alakzatos elmosás + Sharpen Details Részletek élesítése + Sharpen Radius Élesítési sugár + Sharpness Élesség + Shear Nyírás + Short Lines Rövid vonalak + Short Strokes Rövid vonások + Show Backdrop Háttér megjelenítése + Show Mask Maszk megjelenítése + Show Mesh Háló megjelenítése + Show or hide the Notes panel A Jegyzetek panel megjelenítése vagy elrejtése + Sketch Vázlat + Smart Blur Intelligens elmosás + Smart Sharpen Intelligens élesítés + Smoothness Simaság + Smudge Stick Maszatolórúd + Snowflakes Hópelyhek + Source Layer Forrásréteg + Spatter Fröcskölés + Speckle Pöttyös + Speed Sebesség + Spherize Gömbösítés + Spin Blur Pörgetéses elmosás + Sponge Szivacs + Spot: Spotlámpa: + Spray Radius Szórási sugár + Sprayed Strokes Szórt vonások + Sprinkles Szórt szemcsék + Square Size Négyzetméret + sRGB sRGB + Stained Glass Ólomüveg + Stamp Bélyegző + Stars Csillagok + Stippled Pontozott + Stops Átmenetpontok + Straighten Kiegyenesítés + Straighten Layer Réteg kiegyenesítése + Stroke Detail Vonás részletessége + Stroke Direction Vonás iránya + Stroke Length Vonáshossz + Stroke Pressure Vonásnyomás + Stroke Size Vonásméret + Strokes Vonások + Subtract from Kivonás ebből + Subtract from Sample Kivonás a mintából + Sumi-e Sumi-e + Surface Blur Felületi elmosás + Target X Cél X + Target Y Cél Y + Tetrahedral Tetraéderes + Text Position: Szöveg helye: + Texture Textúra + Texture Coverage Textúrafedettség + Texturizer Textúrázó + Thickness Vastagság + This adjustment ({key}) is kept as imported and can't be edited yet. Ez a korrekció ({key}) importált állapotában marad, és egyelőre nem szerkeszthető. + Threshold Küszöb + Threshold Level Küszöbszint + Tile Size Csempeméret + Tiles Csempék + Tilt Dőlés + Tilt-Shift Tilt-shift + Timing Időzítés + Tint Color Színezés színe + Tiny Lens Apró lencsék + Toggle count group visibility Számlálócsoport láthatóságának váltása + Tone Curve Tónusgörbe + Tone: Tónus: + Top Felül + Torn Edges Tépett szélek + Total: Összesen: + Trace Contour Kontúrkövetés + Transparency Threshold Átlátszósági küszöb + Trilinear Trilineáris + Twirl Örvény + Unavailable in this color mode Ebben a színmódban nem érhető el + Undefined Areas Nem definiált területek + Underpainting Alapozó festés + Underwater Víz alatti + Unsharp Mask Életlenmaszk + Use Legacy Régi módszer használata + Use Measurement Scale Mérési méretarány használata + Use Selection In Source Kijelölés használata a forrásban + Use Selection In Target Kijelölés használata a célban + Uses the foreground and background colours. Az előtér- és a háttérszínt használja. + Vertical Only Csak függőleges + Vibrance Élénkség + View Options Nézetbeállítások + Vignette Amount Vignetta mértéke + Vignette Highlights Vignetta csúcsfényei + Vignette Midpoint Vignetta középpontja + Vignette Style Vignetta stílusa + Vine Color Inda színe + Vine With Flowers Inda virágokkal + Vine With Leaves Inda levelekkel + Violent Heves + Violet Ibolya + Warming L B A Melegítőszűrő (LBA) + Warming81 Melegítőszűrő (81) + Warming85 Melegítőszűrő (85) + Water Paper Vizes papír + Watercolor Akvarell + Wave Hullám + Wavelength Max Legnagyobb hullámhossz + Wavelength Min Legkisebb hullámhossz + Waves Hullámok + White Clip Fehér levágása + White Intensity Fehér intenzitása + White Is High Fehér a magas + White Matte Fehér háttér + Whites Fehérek + Wide Blurry Széles, elmosódott + Wide Sharp Széles, éles + Wind Szél + Yellow · Blue Sárga · Kék + Yellows Sárgák + Zigzag Cikcakk + ZigZag Cikcakk + Zoom in Nagyítás + Zoom out Kicsinyítés + {added}, {removed} subtracted {added}, {removed} kivonva + {index} of {total} {index} / {total} + {n} of {max} sample points {n} / {max} mintavételi pont + +# Camera Raw and camera data + After Utána + Aqua Türkiz + Balance Egyensúly + Before (Y) Előtte (Y) + Blending Keverés + Camera Raw Camera Raw + Camera Raw ({name}) Camera Raw ({name}) + Camera Raw Filter Camera Raw szűrő + Camera Raw Filter ({layer}) Camera Raw szűrő ({layer}) + Camera Raw isn't open A Camera Raw nincs megnyitva + Cancelled opening {name} Megnyitás megszakítva: {name} + Cancelled {label} Megszakítva: {label} + Clarity Tisztaság + Color Detail Színrészletesség + Color Grading Színtónusozás + Color Mixer Színkeverő + Color Priority Színprioritás + Curve Görbe + Curve Darks Görbe: sötét tónusok + Curve Highlights Görbe: csúcsfények + Curve Lights Görbe: világos tónusok + Curve Shadows Görbe: árnyékok + Darks Sötét tónusok + Dehaze Párátlanítás + Detail Részletesség + Developing {name} Előhívás: {name} + Feather Elmosott szél + Full-resolution preview unavailable A teljes felbontású előnézet nem érhető el + Global Globális + Grade Balance Színtónusozás: egyensúly + Grade Blending Színtónusozás: keverés + Grade Global Színtónusozás: globális + Grade Highlights Színtónusozás: csúcsfények + Grade Midtones Színtónusozás: középtónusok + Grade Shadows Színtónusozás: árnyékok + Grain Amount Szemcse mértéke + Grain Roughness Szemcse érdessége + Grain Size Szemcseméret + Hand Tool Kéz + Highlight Clipping Warning (O) Figyelmeztetés csúcsfény-levágásra (O) + Highlight Priority Csúcsfény-prioritás + Hsl Hue HSL-színárnyalat + Hsl Lum HSL-luminancia + Hsl Sat HSL-telítettség + Lights Világos tónusok + Lum Lum. + Luminance Luminancia + Luminance Detail Luminancia-részletesség + Masking Maszkolás + Midpoint Középpont + Noise Color Zajcsökkentés: szín + Noise Color Detail Zajcsökkentés: színrészletesség + Noise Luminance Zajcsökkentés: luminancia + Noise Luminance Detail Zajcsökkentés: luminancia-részletesség + Noise Reduction Zajcsökkentés + Open in camera raw Megnyitás a Camera Raw-ban + Paint Overlay Ráfestés + Previous Előző + Purple Lila + Radius Sugár + Reading Beolvasás + Roughness Érdesség + Roundness Kerekség + Sat Tel. + Scope Hatókör + Shadow Clipping Warning (U) Figyelmeztetés árnyéklevágásra (U) + Sharpen Amount Élesítés mértéke + Sharpen Detail Élesítés részletessége + Sharpen Masking Élesítés maszkolása + Sharpening Élesítés + Show Lab Color Readouts Lab-színértékek megjelenítése + Show Red at 3 o'clock Vörös a 3 óra irányában + Show Selected Region Kijelölt terület megjelenítése + Show Skin Tone Indicator Bőrtónusjelző megjelenítése + Show Vectorscope Vektorszkóp megjelenítése + Temperature Színhőmérséklet + Tint Színezet + Tone Histogram Tónushisztogram + Vignette Feather Vignetta elmosott széle + Vignette Roundness Vignetta kereksége + Vignetting Vignettálás + White Balance: As Shot Fehéregyensúly: felvétel szerint + Zoom Tool Nagyító +cameraData Date Taken Készítés ideje +cameraData Exposure Expozíciós idő +cameraData F-Stop Rekesz +cameraData Focal Length Gyújtótávolság +cameraData Focal Length (35 mm) Gyújtótávolság (35 mm) +cameraData ISO Speed ISO-érzékenység +cameraData Lens Objektív +cameraData Make Gyártó +cameraData Model Típus +cameraRaw Amount Mérték +cameraRaw Aqua Türkiz +cameraRaw Blacks Feketék +cameraRaw Blending Keverés +cameraRaw Blue Kék +cameraRaw Color Szín +cameraRaw Color Detail Színrészletesség +cameraRaw Color Grading Színtónusozás +cameraRaw Darks Sötét tónusok +cameraRaw Global Globális +cameraRaw Green Zöld +cameraRaw Hue Színárnyalat +cameraRaw Light Fény +cameraRaw Lights Világos tónusok +cameraRaw Luminance Luminancia +cameraRaw Luminance Detail Luminancia-részletesség +cameraRaw Magenta Bíbor +cameraRaw Masking Maszkolás +cameraRaw Orange Narancs +cameraRaw Purple Lila +cameraRaw Red Vörös +cameraRaw Roughness Érdesség +cameraRaw Temperature Színhőmérséklet +cameraRaw Tint Színezet +cameraRaw Vibrance Élénkség +cameraRaw Whites Fehérek +cameraRaw Yellow Sárga + +# Dialogs, preferences and file formats + % of all commits Commitok aránya (%) + (print to the printer) (nyomtatás a nyomtatón) + 0 turns this threshold off; SVG groups nested deeper than 100 levels are always rasterized. A 0 kikapcsolja ezt a küszöböt; a 100 szintnél mélyebben egymásba ágyazott SVG-csoportok mindig raszterizálódnak. + 12 Columns 12 oszlop + 125% 125% + 150% 150% + 16 bit 16 bites + 175% 175% + 24 bits/pixel 24 bit/pixel + 300% 300% + 32 bit (float) 32 bites (lebegőpontos) + 32 bits/pixel 32 bit/pixel + 3D 3D + 5 Columns, 3 Rows 5 oszlop, 3 sor + 75% 75% + 8 bit 8 bites + 8 Columns 8 oszlop + @{login}: {prs} PRs, {commits} commits, +{added} / −{deleted} lines (Δ {delta}), +{binAdded} / −{binDeleted} binary assets, {first} – {last} @{login}: {prs} PR, {commits} commit, +{added} / −{deleted} sor (Δ {delta}), +{binAdded} / −{binDeleted} bináris elem, {first} – {last} + About Névjegy + Absolute Colorimetric Abszolút kolorimetrikus + Action Művelet + Active Aktív + Adaptive Adaptív + Add disk Lemez hozzáadása + Add to favorites Hozzáadás a kedvencekhez + Add to Swatches Hozzáadás a színmintákhoz + Adobe Adobe + Adobe RGB (1998) Adobe RGB (1998) + Adobe RGB (1998) compatible Adobe RGB (1998)-kompatibilis + Advanced Speciális + Adwaita dark Adwaita sötét + All Mind + All shortcuts reset to PhotoCraft defaults. Minden billentyűparancs visszaállt a PhotoCraft alapértékére. + All Slices Összes szelet + All User Slices Összes felhasználói szelet + Always create smart objects when placing Elhelyezéskor mindig intelligens objektum létrehozása + Answer the unsaved-changes prompt first Előbb válaszolj a nem mentett módosításokra vonatkozó kérdésre + Appearance Settings… Megjelenési beállítások… + Apple Watch 45mm Apple Watch 45 mm + Applies at next launch. A következő indításkor lép életbe. + Art & Illustration Művészet és illusztráció + Artist Alkotó + Ask before saving layered tiff Kérdés rétegzett TIFF mentése előtt + Assign working space Munkaszíntér hozzárendelése + Auto Hide Notifications Értesítések automatikus elrejtése + Automatic (recommended) Automatikus (ajánlott) + Automatic uses GPU acceleration when available and falls back to CPU rendering on errors. Az automatikus mód GPU-gyorsítást használ, ha elérhető, hiba esetén pedig CPU-s renderelésre vált. + Autosave Automatikus mentés + Autosave failed: {error} Az automatikus mentés nem sikerült: {error} + Autosave minutes Automatikus mentés gyakorisága (perc) + Background Color Háttérszín + Background Contents Háttér tartalma + Batch Kötegelt feldolgozás + Binary assets added Hozzáadott bináris elemek + Binary assets removed Eltávolított bináris elemek + Bit depth Bitmélység + Black Point Compensation Feketepont-kompenzáció + BLANK DOCUMENT PRESETS ({n}) ÜRES DOKUMENTUMOK ELŐBEÁLLÍTÁSAI ({n}) + Bleach Bypass Kihagyott fehérítés + Bottom: Alsó: + Breeze dark Breeze sötét + Breeze light Breeze világos + Cancel Mégse + Canvas color Vászonszín + Canvas custom color Egyéni vászonszín + Caption Képaláírás + Center Columns Oszlopok középre igazítása + Center Crop Marks Középső vágójelek + Centimeters Centiméter + Clear Kiürít + Clear Existing Guides Meglévő segédvonalak törlése + Click to restore the current colour Kattints a jelenlegi szín visszaállításához + Cmd Cmd + CMYK Color CMYK-szín + Coated CMYK Mázolt papíros CMYK + Color Adaptation Színadaptáció + Color Management Színkezelés + Color Mode Színmód + Color Picker (Background Color) Színválasztó (háttérszín) + Color Picker (Fill Color) Színválasztó (kitöltőszín) + Color Picker (Foreground Color) Színválasztó (előtérszín) + Color Picker ({name}) Színválasztó ({name}) + Color Profile Színprofil + Color space Színtér + Color… Szín… + Column width Oszlopszélesség + Columns Oszlopok + Comic Book Képregény + Command Parancs + command or shortcut parancs vagy billentyűparancs + Commits Commitok + Company Cég + Compare Összehasonlítás + Compatibility Kompatibilitás + Compression Tömörítés + Conditional Mode Change Feltételes módváltás + Contact Sheet II Kontaktlap II + Content-Aware Fill needs a selection on a pixel layer A tartalomérzékeny kitöltéshez kijelölés kell egy pixelrétegen + Contents: Tartalom: + Contributors Közreműködők + Contributors: {contributors} · Commits: {commits} Közreműködők: {contributors} · Commitok: {commits} + Convert document's colors to the working space A dokumentum színeinek átalakítása a munkaszíntérbe + Convert to sRGB Átalakítás sRGB-re + Cool Hideg + Copies Példányszám + Copyright Szerzői jog + Copyright and Contact Info Szerzői jog és elérhetőség + Copyrighted Szerzői jog által védett + Corner Crop Marks Sarokvágójelek + Couldn't open {name}: {error} A fájl nem nyitható meg: {name} ({error}) + Couldn't place {name}: {error} A fájl nem helyezhető el: {name} ({error}) + Couldn't save preferences: {error}. Retrying… A beállítások nem menthetők: {error}. Újrapróbálkozás… + CPU (no GPU acceleration) CPU (GPU-gyorsítás nélkül) + CPU / Compatibility CPU / kompatibilitás + Create Létrehoz + Create Droplet Droplet létrehozása + Cross Process Kereszthívás + Ctrl Ctrl + Custom Pattern: Egyéni mintázat: + Custom Shapes Egyéni alakzatok + Dark Theme Sötét téma + Data Adatok + Date Dátum + Day For Night Nappali éjszaka + Default PhotoCraft Size Alapértelmezett PhotoCraft-méret + Default printer Alapértelmezett nyomtató + Default restored. Alapértelmezés visszaállítva. + Defaults Alapértékek + Delete Törlés + Delete Shortcut Billentyűparancs törlése + Description Leírás + Diffusion Diffúzió + DirectX 12 DirectX 12 + Discard Layers and Save a Copy Rétegek elvetése és másolat mentése + Discard the embedded profile (don't color manage) Beágyazott profil elvetése (nincs színkezelés) + Disks Lemezek + Display name Megjelenített név + Display P3 Display P3 + Display p3 Display P3 + Do you want to save the changes you made to “{name}” before closing? Mented a módosításokat bezárás előtt? Dokumentum: „{name}” + Do you want to save the changes you made to “{name}” before quitting? Mented a módosításokat kilépés előtt? Dokumentum: „{name}” + Document Dokumentum + Don't Import Importálás nélkül + Don't Save Mentés nélkül + Edit › Keyboard Shortcuts › Reset All to Defaults undoes an import. Az importálást a Szerkesztés › Billentyűparancsok › Összes visszaállítása alapértékre vonja vissza. + Embed Color Profile Színprofil beágyazása + Embedded Profile Mismatch Eltérő beágyazott profil + Emulsion Down Emulziós oldal lefelé + Error Hiba + Exact Pontos + EXIF EXIF + Export As Exportálás másként + Export Options Exportálás beállításai + Export Preferences Beállítások exportálása + Export Presets Előbeállítások exportálása + Export Settings Exportálási beállítások + Export/Import Presets Előbeállítások exportálása/importálása + Exported presets to {path} Előbeállítások exportálva. Cél: {path} + Exported {path} Exportálva: {path} + Exported {path} ({size}) Exportálva: {path} ({size}) + Extensions Kiterjesztések + Faded Film Fakult film + File Info Fájladatok + File Name Fájlnév + File path Fájl elérési útja + File Settings Fájlbeállítások + Fill color Kitöltőszín + Film & Video Film és videó + Finish Befejezés + First Első + First commit Első commit + Fit Image Kép méretre igazítása + Floating point Lebegőpontos + Focal Length35mm Gyújtótávolság (35 mm) + Foreground Color Előtérszín + Format Formátum + Functions Funkciók + Gamut warning color Színtartomány-figyelmeztetés színe + Gamut warning opacity Színtartomány-figyelmeztetés átlátszatlansága + Gif GIF + GPU GPU + GPU Backend GPU-háttérrendszer + Grab bag Felhő + Graphics Grafika + Gray Gamma 2.2 Szürke, gamma 2.2 + Grayscale Szürkeárnyalatos + Guide color Segédvonal színe + Guide Layout… Segédvonal-elrendezés… + Gutter Hasábköz + Image previews Képelőnézetek + Image Size Képméret + Import brushes (.abr) Ecsetek importálása (.abr) + Import keyboard shortcuts? Importálod a billentyűparancsokat? + Import Presets Előbeállítások importálása + Import Shortcuts… Billentyűparancsok importálása… + Imported {n} shortcuts from {file}. Click OK to keep them. {n} billentyűparancs importálva. Forrás: {file}. A megtartásukhoz kattints az OK gombra. + Inches Hüvelyk + Including layers will increase file size. A rétegek mentése növeli a fájlméretet. + Integer Egész + Interlaced Váltott soros + Invalid guide layout Érvénytelen segédvonal-elrendezés + ISO Speed ISO-érzékenység + Jpeg quality JPEG-minőség + Jpg JPG + Keyboard Shortcuts and Menus Billentyűparancsok és menük + Keyboard shortcuts imported Billentyűparancsok importálva + Keywords Kulcsszavak + Lab Color Lab-szín + Labels Címkék + Landscape Fekvő + Landscape, 10 x 8 Fekvő, 10 x 8 + Landscape, 6 x 4 Fekvő, 6 x 4 + Landscape, 7 x 5 Fekvő, 7 x 5 + Last Utolsó + Last commit Utolsó commit + Leave as is (don't color manage) Változatlanul hagyás (nincs színkezelés) + Leave empty for automatic size Automatikus mérethez hagyd üresen + Left Bal + Left: Bal: + Legal Legal + Lens Objektív + Light Theme Világos téma + Line delta Sorváltozás + Lines +/− Sorok +/− + Lines added Hozzáadott sorok + Lines deleted Törölt sorok + Load a keyboard shortcut set saved as .kys .kys formátumban mentett billentyűparancs-készlet betöltése + Lossless Veszteségmentes + Lowercase extension Kisbetűs kiterjesztés + Make Gyártó + Margin Margó + Matte Alapszín + Merged PRs Beolvasztott PR-ek + Metadata Metaadatok + Metal Metal + Millimeters Milliméter + Mobile Mobil + Model Modell + Models Modellek + modified módosítva + Mono Contrast Kontrasztos monokróm + Name (A–Z) Név (A–Z) + New Document Új dokumentum + New Guide Layout Új segédvonal-elrendezés + No Nem + No Color Management Nincs színkezelés + No contributor data was built into this copy. Ez a példány nem tartalmaz közreműködői adatokat. + No Dither Nincs árnyalatkeverés + No matching settings Nincs egyező beállítás + No model credits were built into this copy. Ez a példány nem tartalmaz adatokat a modellek készítőiről. + No presets of this type. Ilyen típusú előbeállítás nincs. + No saved presets yet. Még nincs mentett előbeállítás. + Not a web colour: click for the nearest web colour Nem webes szín: kattints a legközelebbi webes színért + Notification Duration (seconds) Értesítés időtartama (másodperc) + Number Szám + OK OK + Only Web Colors Csak webes színek + Open File Fájl megnyitása + OpenGL OpenGL + Optimized Optimalizált + Original Eredeti + Out of gamut for printing: click for the nearest printable colour Nyomtatási színtartományon kívül: kattints a legközelebbi nyomtatható színért + Out of memory Elfogyott a memória + Package Csomagolás + Percent Százalék + Perceptual Érzékelési + Permission denied Hozzáférés megtagadva + Phone Telefon + Photo Fotó + PhotoCraft found your keyboard shortcut set {set} from another image editor on this computer: {n} of its shortcuts differ from PhotoCraft's. Import it? A PhotoCraft egy másik képszerkesztő billentyűparancs-készletét találta ezen a számítógépen: {set}. Ebben {n} billentyűparancs eltér a PhotoCraftétól. Importálod? + PhotoCraft keeps retrying; until a save succeeds, preference changes are lost when it closes. A PhotoCraft tovább próbálkozik; amíg a mentés nem sikerül, a beállítások módosításai bezáráskor elvesznek. + PhotoCraft Manages Colors A PhotoCraft kezeli a színeket + PhotoCraft — an open-source, native image editor written in Rust. PhotoCraft – nyílt forráskódú, natív képszerkesztő Rust nyelven. + Picas Pica + Pixels Pixel + Pixels/Centimeter Pixel/centiméter + Pixels/Inch Pixel/hüvelyk + Place Embedded Beágyazott elhelyezése + Place Linked Csatolt elhelyezése + Png PNG + Points Pont + Portrait Álló + Portrait, 4 x 6 Álló, 4 x 6 + Portrait, 5 x 7 Álló, 5 x 7 + Position and Size Pozíció és méret + Postcard Képeslap + Poster Plakát + PostScript (72 points/inch) PostScript (72 pont/hüvelyk) + Preferences can't be saved A beállítások nem menthetők + Preferences saved Beállítások mentve + Preferences were reset: {error} A beállítások alaphelyzetbe álltak: {error} + Preset Előbeállítás + PRESET DETAILS AZ ELŐBEÁLLÍTÁS RÉSZLETEI + Preset Manager Előbeállítás-kezelő + Preset Name Előbeállítás neve + Preset Type Előbeállítás típusa + Press keys Nyomd le a billentyűket + Press keys… Nyomd le a billentyűket… + Print Nyomtatás + Print resolution Nyomtatási felbontás + Print Resolution: Nyomtatási felbontás: + Printed to {path} Nyomtatva. Cél: {path} + Printer Nyomtató + Printer Manages Colors A nyomtató kezeli a színeket + Printer Setup Nyomtatóbeállítás + Printing Marks Nyomdai jelek + Progressive Progresszív + PSD PSD + Public Domain Közkincs + Quality Minőség + Quick export format Gyors exportálás formátuma + Ran script {name} Parancsfájl lefuttatva: {name} + Real name Valódi név + Recent Legutóbbiak + Recover on launch Helyreállítás indításkor + Recovered {name} Helyreállítva: {name} + Recovery Helyreállítás + Registration Marks Illesztőjelek + Relative Colorimetric Relatív kolorimetrikus + Remove from favorites Eltávolítás a kedvencek közül + Rename Átnevezés + Render Video Videó renderelése + Rendering Mode Renderelési mód + Reset All to Defaults Összes visszaállítása alapértékre + Reset Section Szakasz visszaállítása + Resize image during place Kép átméretezése elhelyezéskor + Resolution Felbontás + Resolution: {dpi} pixels/inch Felbontás: {dpi} pixel/hüvelyk + Restore Defaults Alapértékek visszaállítása + Restrictive (Web) Korlátozott (web) + Result Eredmény + Retry Újrapróbál + Reverse the order Sorrend megfordítása + Revert “{name}” to the last saved version? Your changes will be lost. Visszaállítod az utolsó mentett változatot? A módosításaid elvesznek. Dokumentum: „{name}” + RGB Color RGB-szín + Right: Jobb: + Rows Sorok + Rulers & Grids Vonalzók és rácsok + Save a Copy Másolat mentése + Save as PDF Mentés PDF-ként + Save cancelled Mentés megszakítva + Save failed: {error} A mentés nem sikerült: {error} + Save for Web (Legacy) Mentés webre (régi) + Save HTML and Images HTML és képek mentése + Save Preset… Előbeállítás mentése… + Saved Mentve + Scale to Fit Media Méretezés a papírmérethez + Scratch disks Lapozólemezek + Script Events Manager Parancsfájl-eseménykezelő + Search Keresés + Search settings… Keresés a beállítások között… + Selected Artboards Kijelölt rajztáblák + Selective Szelektív + Sensitivity Érzékenység + Sent to the printer ({spooler}) Elküldve a nyomtatóra ({spooler}) + Services Szolgáltatások + Sessions only Csak munkamenetek + Shape Layer Alakzatréteg + Shortcut removed. Billentyűparancs eltávolítva. + Shortcuts Billentyűparancsok + Show All Menu Items Összes menüpont megjelenítése + Show Guides Segédvonalak megjelenítése + Show rulers Vonalzók megjelenítése + Show Rulers Vonalzók megjelenítése + Show tooltips Eszköztippek megjelenítése + Software Szoftver + Solarized dark Solarized sötét + Sort Rendezés + Square, 12 x 12 Négyzet, 12 x 12 + Square, 5 x 5 Négyzet, 5 x 5 + Srgb sRGB + sRGB IEC61966-2.1 sRGB IEC61966-2.1 + Studio light Studio világos + Studio Light Studio világos + System Info Rendszerinformáció + Table Táblázat + Tabloid Tabloid + Targa Options Targa-beállítások + Target: Cél: + Teal Orange Kékeszöld-narancs + The document does not have an embedded colour profile. Working space: {working}. A dokumentumnak nincs beágyazott színprofilja. Munkaszíntér: {working}. + The document has an embedded colour profile that does not match the working space.\nEmbedded: {embedded}\nWorking: {working} A dokumentum beágyazott színprofilja eltér a munkaszíntértől.\nBeágyazott: {embedded}\nMunkaszíntér: {working} + The keyboard shortcut set was not imported: {error} A billentyűparancs-készlet importálása nem sikerült: {error} + These settings aren't available in PhotoCraft yet. Ezek a beállítások a PhotoCraftban még nem érhetők el. + This fill layer must be rasterized before proceeding. Its fill content will no longer be editable. Rasterize the layer? A folytatáshoz a kitöltőréteget raszterizálni kell. A kitöltés tartalma ezután nem lesz szerkeszthető. Raszterizálod a réteget? + This layer must be rasterized before proceeding. A folytatáshoz a réteget raszterizálni kell. + This shape layer must be rasterized before proceeding. It will no longer be editable as a shape. Rasterize the shape? A folytatáshoz az alakzatréteget raszterizálni kell. Ezután alakzatként nem lesz szerkeszthető. Raszterizálod az alakzatot? + This Smart Object must be rasterized before proceeding. Its contents will no longer be editable. Rasterize the Smart Object? A folytatáshoz az intelligens objektumot raszterizálni kell. A tartalma ezután nem lesz szerkeszthető. Raszterizálod az intelligens objektumot? + This type layer must be rasterized before proceeding. Its text will no longer be editable. Rasterize the type? A folytatáshoz a szövegréteget raszterizálni kell. A szöveg ezután nem lesz szerkeszthető. Raszterizálod a szöveget? + TIFF Options TIFF-beállítások + Top: Felső: + Traditional (72.27 points/inch) Hagyományos (72,27 pont/hüvelyk) + Transparency Átlátszóság + Unix Unix + Unsaved changes Nem mentett módosítások + Untitled Névtelen + Untitled-1 Névtelen-1 + URLs URL-ek + Use Default Alapértelmezés használata + Use shift key for tool switch Eszközváltás a Shift billentyűvel + Use the embedded profile (instead of the working space) A beágyazott profil használata (a munkaszíntér helyett) + Username Felhasználónév + Version {version} Verzió: {version} + Vulkan Vulkan + Warm Meleg + Web Web + Web Color Webes szín + Web Large Web, nagy + Web Minimum Web, minimális + Web Most Common Web, leggyakoribb + Web Snap Webes illesztés + Webp WebP + Wetness Nedvesség + White Fehér + Width and Height Szélesség és magasság + Working Munkaszíntér + Y Y + Yes Igen + Your keyboard shortcut set {set} is in use here: {n} shortcuts differ from the defaults. Edit › Keyboard Shortcuts shows them; Reset All to Defaults undoes this. A billentyűparancs-készleted itt is használatban van: {set}. Ebből {n} billentyűparancs eltér az alapértelmezettől. A Szerkesztés › Billentyűparancsok alatt láthatod őket; az Összes visszaállítása alapértékre paranccsal ez visszavonható. + {n} commands are not in PhotoCraft. {n} parancs nem érhető el a PhotoCraftban. + {shortcut} assigned. Billentyűparancs hozzárendelve: {shortcut} + {shortcut} is already in use by {commands} and will be removed from it when you click OK. A billentyűparancs már foglalt: {shortcut}. Jelenlegi parancs: {commands}. Az OK gombra kattintva onnan törlődik. + +# Status bar, messages and other labels + Action playback failed: {error} A műveletsor lejátszása sikertelen: {error} + Adaptive Wide Angle Adaptív nagy látószög + Add Anchor Point Tool Horgonypont hozzáadása + Add beauty precomp from the AOVs Beauty előkompozíció hozzáadása az AOV-kből + Add to Hozzáadás + Additional plugins folder További bővítmények mappája + Adjustment Layer Korrekciós réteg + Adjustment Properties Korrekció tulajdonságai + Adobe rgb Adobe RGB + Adwaita Adwaita + Adwaita (Dark) Adwaita (sötét) + Adwaita (Light) Adwaita (világos) + Allow agent control Ágensvezérlés engedélyezése + Allow scripts to connect Parancsfájlok csatlakozásának engedélyezése + Alt-click went to the desktop, which moves windows with Alt-drag. Set its window-move key to Super, or latch Alt in Window › Modifier Keys. Az Alt+kattintást az asztali környezet kapta meg, mert nála az Alt+húzás ablakot mozgat. Állítsd az ablakmozgató billentyűt a Super billentyűre, vagy rögzítsd az Alt billentyűt itt: Ablak › Módosítóbillentyűk. + Ambience Környezeti fény + Ambient Környezeti + Anchor: Rögzítési pont: + Animated zoom Animált nagyítás + Anisotropic Anizotróp + Apply At Canvas Bounds Alkalmazás a vászon szélén is + Apply auto tone Automatikus tónus alkalmazása + Apply Layer Comp Rétegkompozíció alkalmazása + Arc Ív + Arch Boltív + Area Terület + Artboard Tool Rajztábla + ArtCraft ArtCraft + Ask Rákérdezés + Aspect Ratio Oldalarány + Assign Profile Profil hozzárendelése + Author Szerző + auto automatikus + Auto collapse icon panels Ikonpanelek automatikus összecsukása + Auto Scale Automatikus méretezés + Auto show hidden panels Rejtett panelek automatikus megjelenítése + Auto show home screen Kezdőképernyő automatikus megjelenítése + Auto-Align Layers Rétegek automatikus igazítása + Auto-Blend Layers Rétegek automatikus keverése + Automatic Automatikus + Back Vissza + Backend: {backend} ({type}) Háttérrendszer: {backend} ({type}) + Background Háttér + Background Eraser Tool Háttérradír + Begin recording Rögzítés indítása + Bicubic (smooth gradients) Bikubikus (sima átmenetekhez) + Bicubic automatic Automatikus bikubikus + Bicubic sharper Élesebb bikubikus + Bicubic Sharper (reduction) Élesebb bikubikus (kicsinyítéshez) + Bicubic smoother Simább bikubikus + Bicubic Smoother (enlargement) Simább bikubikus (nagyításhoz) + Blade Curvature Lamellák görbülete + Blast Szélroham + Blend Keverés + Border Szegély + Bottom Left Bal alsó + Bottom Right Jobb alsó + Breeze Dark Breeze sötét + Breeze Light Breeze világos + Browse Tallózás + Bulge Kidudorodás + Cache levels Gyorsítótár-szintek + Cache tile size Gyorsítótár csempemérete + Camera Data Fényképezőgép adatai + Camera Raw Defaults Camera Raw-alapértékek + Camera Raw… Camera Raw… + Cancel opening Megnyitás megszakítása + Canvas border Vászonszegély + Canvas extension color: Vászonbővítés színe: + Canvas Size Vászonméret + Category Kategória + Channel1 1. csatorna + Channel2 2. csatorna + Channel3 3. csatorna + Channel4 4. csatorna + Circle Kör + Circularity Köralakúság + Classic Klasszikus + Cleanliness Vonaltisztaság + Click a segment to add an anchor point · {alt}-click an anchor to delete it Kattints egy szakaszra horgonypont hozzáadásához · {alt}+kattintás egy horgonyponton: törlés + Click an anchor point to delete it · {alt}-click a segment to add one Kattints egy horgonypontra a törléséhez · {alt}+kattintás egy szakaszon: új horgonypont + Click to pick · Esc to cancel Kattintás: kiválasztás · Esc: mégse + Click: corner · Drag: smooth · {alt}-drag: break handle · Click or drag first point: close · {key} finish · Esc cancel Kattintás: sarokpont · Húzás: simított pont · {alt}+húzás: fogantyú megtörése · Kattintás vagy húzás az első ponton: lezárás · {key}: befejezés · Esc: mégse + Click: select anchor · Drag: move anchor, handle or segment · {shift}-click: add · {alt}-click: whole subpath Kattintás: horgonypont kijelölése · Húzás: horgonypont, fogantyú vagy szakasz mozgatása · {shift}+kattintás: hozzáadás · {alt}+kattintás: teljes részútvonal + Click: smooth point · Double-click: corner · Drag a point: move · Click first point: close · {key} finish Kattintás: simított pont · Dupla kattintás: sarokpont · Pont húzása: mozgatás · Kattintás az első ponton: lezárás · {key}: befejezés + Clipping Path Vágóútvonal + Clone Klónozás + Clone Stamp Tool Klónozó bélyegző + Close Bezár + Color management: {error} Színkezelés: {error} + Color picker Színválasztó + Color Picker Színválasztó + Color Range Színtartomány + Color Replacement Tool Színcserélő + Color Settings Színbeállítások + Color Table Színtábla + ColorSync ColorSync + Compose Szedés + Concise Tömör + Constrain proportions Arányok megtartása + Constraints Megkötések + Content-Aware Fill Tartalomérzékeny kitöltés + Content-Aware Move Tool Tartalomérzékeny mozgatás + Contract Szűkítés + Control port Vezérlőport + Convert Point Tool Pontátalakító + Convert to Linked Átalakítás csatolttá + Convert to Profile Átalakítás profilra + Convert to srgb Átalakítás sRGB-re + Copyright Status Szerzői jogi állapot + Corner radius Sarokkerekítés sugara + Correct C A Kromatikus aberráció javítása + Correct Distortion Torzítás javítása + Correct Vignette Vignettálás javítása + Could not rename the action: {error} A műveletsor nem nevezhető át: {error} + Count Tool Számláló + Create a new document or open an existing file. Hozz létre egy új dokumentumot, vagy nyiss meg egy meglévő fájlt. + Create By Pótlás módja + Create new action Új műveletsor létrehozása + Create new path Új útvonal létrehozása + Crop Tool Körbevágás + Cross Kereszt + Current Size Jelenlegi méret + Current Tool Aktuális eszköz + Cursors Kurzorok + Curvature Pen Tool Görbületi toll + Curve Fit: Görbeillesztés: + Custom dark Egyéni sötét + Custom light Egyéni világos + Custom Shape Tool Egyéni alakzat + Dashed lines Szaggatott vonalak + Date and Time Dátum és idő + Default Foreground and Background Colors Alapértelmezett előtér- és háttérszín + Delete Anchor Point Tool Horgonypont törlése + Delete path Útvonal törlése + Delete Path Útvonal törlése + Delete vector mask Vektormaszk törlése + Depth Mode Mélységmód + Detailed Részletes + Details Részletek + Dimensions: Méretek: + Direct Selection Tool Közvetlen kijelölés + Disable Kikapcsolás + Disabled Kikapcsolva + Dismiss Elvetés + Display Kijelző + Display profile not used A kijelzőprofil nincs használatban + Display profiles: {reason} Kijelzőprofilok: {reason} + Distribution Eloszlás + Doc: {flat}/{layered} Dokumentum: {flat}/{layered} + Document Dimensions Dokumentum méretei + Document Profile Dokumentumprofil + Document Size Dokumentumméret + Document Sizes Dokumentumméretek + Documents Dokumentumok + Draft Vázlat + Drag to move the active shape's path or the Work Path Húzással mozgathatod az aktív alakzat útvonalát vagy a munkaútvonalat + Drag: draw freehand · {alt}-click: straight segment · Release on the start point: close Húzás: szabadkézi rajzolás · {alt}+kattintás: egyenes szakasz · Elengedés a kezdőponton: lezárás + Draw with the Pen tool (P) or make a work path from a selection. Rajzolj a Toll eszközzel (P), vagy készíts munkaútvonalat egy kijelölésből. + Driver: {driver} Illesztőprogram: {driver} + Drop an image or PSD anywhere to open it. Húzz ide bárhová egy képet vagy PSD-fájlt a megnyitásához. + Duplicate Layer Comp Rétegkompozíció kettőzése + Duplicate Path Útvonal kettőzése + Duplication Kettőzés + East asian Kelet-ázsiai + Edge Extension Szélek kiterjesztése + Edit › Paste Szerkesztés › Beillesztés + Edits that change the whole document, such as a layer's blend mode, opacity or visibility, redraw all of it. A teljes dokumentumot érintő módosítások (például egy réteg keverési módja, átlátszatlansága vagy láthatósága) az egész dokumentumot újrarajzolják. + Effect cache mb Effektus-gyorsítótár (MB) + Efficiency Hatékonyság + Efficiency: 100% Hatékonyság: 100% + Elevation Magassági szög + Eliminate Eltávolítandó + Ellipse Tool Ellipszis + Elliptical Marquee Tool Ellipszis-kijelölés + Enable Bekapcsolás + Enable flick panning Lendítéses pásztázás engedélyezése + Enable floating document window docking Lebegő dokumentumablakok dokkolásának engedélyezése + Enable narrow options bar Keskeny beállítósáv engedélyezése + Enabled Bekapcsolva + End X Vég X + End Y Vég Y + Enhanced Controls Bővített vezérlők + Eraser Tool Radír + Errors Hibák + Even Fields Páros félképek + Export Exportálás + Export clipboard Vágólap exportálása + Export Contents Tartalom exportálása + Exported contents to {path} Tartalom exportálva. Cél: {path} + Extra large Nagyon nagy + Eyedropper Tool Pipetta + Failed Sikertelen + fallback tartalék + Fallback: {reason} Tartalék: {reason} + File Handling Fájlkezelés + Filename Fájlnév + Fill Kitöltés + Fill Color Kitöltőszín + Fill Path Útvonal kitöltése + Fill path with foreground color Útvonal kitöltése előtérszínnel + Find Keresés + Fine Finom + Fish Hal + Flag Zászló + Flame Láng + Flat Lapos + Focus Fókusz + Focus Area Fókuszterület + Font preview Betűtípus-előnézet + Frame Keret + Frame Tool Keret + Freeform Pen Tool Szabadkézi toll + From Left Balról + Full size tip Teljes méretű hegy + Fullscreen Teljes képernyő + General… Általános… + Generator enabled Generátor bekapcsolva + Generators Generátorok + Generic Általános + GitHub GitHub + Gloss Csillogás + Glossy Fényes + Good Jó + GPU acceleration could not continue. Your documents are unchanged. A GPU-gyorsítás leállt. A dokumentumok nem változtak. + GPU backend preference: {backend} Előnyben részesített GPU-háttérrendszer: {backend} + GPU device was lost; using the CPU renderer. A GPU-eszköz elérhetetlenné vált; a CPU-s megjelenítő működik. + GPU error; using the CPU renderer. GPU-hiba; a CPU-s megjelenítő működik. + GPU retry scheduled GPU-újrapróbálás ütemezve + Gradient Tool Színátmenet + Graphics adapter Grafikus adapter + Graphics adapter: {adapter} Grafikus adapter: {adapter} + Gray Value (Maximum) Szürkeérték (maximum) + Gray Value (Mean) Szürkeérték (átlag) + Gray Value (Median) Szürkeérték (medián) + Gray Value (Minimum) Szürkeérték (minimum) + Grid color Rács színe + Grid colors Rács színei + Grid style Rács stílusa + Grid unit Rács mértékegysége + Gridline every Rácsvonalak távolsága + Group Layer Csoportréteg + Groups Csoportok + Guide style Segédvonalak stílusa + Guides, Grid & Slices Segédvonalak, rács és szeletek + Hand Kéz + Heal Javítás + Healing Brush Tool Javítóecset + Heptagon Hétszög + Hexagon Hatszög + Hidden Rejtett + High Magas + History Brush Tool Előzményecset + History Log Előzménynapló + History states Előzményállapotok + Home Kezdőképernyő + Horizontal Type Mask Tool Vízszintes szövegmaszk + Horizontal Type Tool Vízszintes szöveg + Hotspot Fényfolt + Hsl HSL + Ignore exif profile tag EXIF-profilcímke figyelmen kívül hagyása + Image compositor: {processor} Képösszeállító: {processor} + Image interpolation Képinterpoláció + Image Size: Képméret: + Image Size… Képméret… + Import Importálás + In Kezdőpont + Include in playback Lejátszásba foglalás + Indexed Indexelt + Infinite Végtelen + Inflate Felfújás + Input Bemenet + Input Mode Bemeneti mód + Integrated Density Integrált sűrűség + Integrations Integrációk + Integrations… Integrációk… + Interface Felület + Interpolation Interpoláció + Interpret Footage Videoanyag értelmezése + Interval Intervallum + Invalid {mode} profile Érvénytelen profil ({mode}) + Its layers don't fit the GPU memory budget. Raising Memory usage in Preferences › Performance raises the budget, up to a quarter of this computer's memory. A dokumentum rétegei nem férnek el a GPU memóriakeretében. Ha a Beállítások › Teljesítmény alatt növeled a memóriahasználatot, a keret is nő, legfeljebb a számítógép memóriájának negyedéig. + Join us on Discord Csatlakozz hozzánk a Discordon + Keep Using CPU Marad a CPU + Kernel Szűrőmag + Keyboard Billentyűzet + Label Címke + Lanczos (sharp) Lanczos (éles) + Large document: redrawing on the CPU Nagy dokumentum: újrarajzolás a CPU-n + Large tabs Nagy lapfülek + Lasso Tool Lasszó + Layer Comp Rétegkompozíció + Layer Count Rétegek száma + Layer From Background Réteg a háttérből + Layer Mask Rétegmaszk + Layer Properties Réteg tulajdonságai + Legacy compositing Régi képösszeállítás + Length Hossz + Lens Correction Objektívkorrekció + Libraries Könyvtárak + Light Type Fényforrás típusa + Light X Fényforrás X + Light Y Fényforrás Y + Light Z Fényforrás Z + Lighting Megvilágítás + Line Vonal + Line Tool Vonal + Linux display server Linuxos megjelenítőszerver + Liquify Képlékenyítés + List Lista + Load path as a selection Útvonal betöltése kijelölésként + Lock All Összes zárolása + Locked Zárolva + Low Alacsony + Low resolution previews Alacsony felbontású előnézetek + Lower Alsó + Magic Eraser Tool Varázsradír + Magic Wand Tool Varázspálca + Magnetic Lasso Mágneses lasszó + Magnetic Lasso Tool Mágneses lasszó + Make Selection Kijelölés készítése + Make work path from selection Munkaútvonal készítése a kijelölésből + Manual Kézi + manual kézi + Map Layer Térképréteg + Map Path Térképfájl + Mask Maszk + Mask Incomplete Blocks Hiányos tömbök maszkolása + Maximize Teljes méret + Maximize psd compatibility PSD-kompatibilitás maximalizálása + Memory usage mb Memóriahasználat (MB) + Metallic Fémesség + Middle Közép + Missing glyph protection Hiányzó glifák elleni védelem + Mixer Brush Tool Keverőecset + Modern Modern + Monitor profile in use on {display}: {profile} Használt monitorprofil ({display}): {profile} + Monitor profile in use: {profile} Használt monitorprofil: {profile} + Monitor profile on {display} (main window): {profile} Monitorprofil ({display}, főablak): {profile} + Monitor profile on {display}: {profile} Monitorprofil ({display}): {profile} + Monitor profile: {profile} Monitorprofil: {profile} + Monochromatic Egyszínű + More documents További dokumentumok + More panels További panelek + Move Layer Réteg mozgatása + Move Tool Mozgatás + name név + Native file drag-and-drop is not supported on Wayland yet. Use File › Open, or copy the image in your file manager and paste it with {paste}. Wayland alatt a natív fájlbehúzás még nem támogatott. Használd a Fájl › Megnyitás… parancsot, vagy másold a képet a fájlkezelőben, és illeszd be ({paste}). + Native file drag-and-drop is unavailable A natív fájlbehúzás nem érhető el + Nearest neighbor Legközelebbi szomszéd + Nearest Neighbor (hard edges) Legközelebbi szomszéd (éles szélek) + New Új + new új + New document Új dokumentum + New Document With Mask Új dokumentum maszkkal + New document… Új dokumentum… + New Group Új csoport + New Guide Új segédvonal + New Layer Új réteg + New Layer Comp Új rétegkompozíció + New Layer With Mask Új réteg maszkkal + New Selection Új kijelölés + New Size Új méret + New Video Layer from File Új videoréteg fájlból + No Document Nincs dokumentum + No layers Nincs réteg + No path to make a selection from: draw one with the Pen tool or select one in the Paths panel. Nincs útvonal, amelyből kijelölés készülhetne: rajzolj egyet a Toll eszközzel, vagy válassz egyet az Útvonalak panelen. + Normal tip Normál hegy + Note Tool Jegyzet + Note tool Jegyzet + Nothing to paste: the clipboard holds no image Nincs mit beilleszteni: a vágólapon nincs kép + Object Selection Tool Objektumkijelölés + Octagon Nyolcszög + Odd Fields Páratlan félképek + On Be + on be + Open as smart object Megnyitás intelligens objektumként + Open documents as tabs Dokumentumok megnyitása lapfülként + open source nyílt forráskódú + Opened {name} Megnyitva: {name} + Opened {url} Megnyitva: {url} + Opening… Megnyitás… + Or set Preferences › Performance › Linux display server to X11: PhotoCraft then always starts under XWayland. Vagy állítsd X11-re a Beállítások › Teljesítmény › Linuxos megjelenítőszerver értékét: a PhotoCraft ekkor mindig XWayland alatt indul. + Original Ratio Eredeti arány + Other Egyéb + Out Végpont + Output Kimenet + Overscroll Túlgörgetés + Paint Bucket Tool Festékesvödör + Patch Tool Foltozó + Path Útvonal + Path Selection Tool Útvonal-kijelölés + Path: Útvonal: + Pattern Stamp Tool Mintázatbélyegző + Pen Tool Toll + Pencil Tool Ceruza + Pentagon Ötszög + Percentage Százalék + Performance Teljesítmény + Perimeter Kerület + Perspective Crop Tool Perspektivikus körbevágás + PhotoCraft is using the CPU image compositor. The window may still use your graphics adapter. A PhotoCraft a CPU-s képösszeállítót használja. Az ablak megjelenítése ettől még használhatja a grafikus adaptert. + PhotoCraft website PhotoCraft-weboldal + PhotoCraft — Screen Color Picker PhotoCraft — Képernyő-színválasztó + Photomerge Fotóegyesítés + Pick a screen pixel or press Esc to cancel Válassz egy pixelt a képernyőn, vagy nyomd meg az Esc billentyűt a megszakításhoz + Pick screen color Szín kiválasztása a képernyőről + Pins Tűk + Pixel Layer Pixelréteg + Place Scale Marker Méretarány-jelölő elhelyezése + Plane Sík + Planes Síkok + Platform: {os} {arch} Platform: {os} {arch} + Play selection Kijelöltek lejátszása + Point Pont + Point size Pontméret + Polygon Tool Sokszög + Polygonal Lasso Tool Sokszöglasszó + Preserve Megőrzendő + Preserve Details Részletek megőrzése + Preserve details Részletek megőrzése + previous reading kept; the last re-read failed: {reason} az előző érték maradt érvényben; a legutóbbi újraolvasás sikertelen: {reason} + Prime105 105 mm-es fix + Prime35 35 mm-es fix + Pro (Dark) Pro (sötét) + Pro (Light Gray) Pro (világosszürke) + Pro (Light) Pro (világos) + Pro (Medium Gray) Pro (középszürke) + Pro light Pro világos + Pro light gray Pro világosszürke + Pro medium Pro középszürke + Pro Medium Pro középszürke + Profile Profil + Pyramids Gúlák + Quick export location Gyors exportálás helye + Quick Selection Gyorskijelölés + Quick Selection Tool Gyorskijelölés + Random Véletlenszerű + Rasterize svg groups deeper than SVG-csoportok raszterizálása ennél mélyebb szinten + Recent file count Legutóbbi fájlok száma + Recent files Legutóbbi fájlok + Recent Files Legutóbbi fájlok + Recent fonts Legutóbbi betűtípusok + Record ● a sequence of edits, then play ▶ it on any document. Rögzíts ● egy szerkesztési sorozatot, majd játszd le ▶ bármelyik dokumentumon. + Rectangle Téglalap + Rectangle Tool Téglalap + Rectangular Marquee Tool Téglalap-kijelölés + Rectangular Selection Téglalap-kijelölés + Red Eye Vörösszem-javítás + Red Eye Tool Vörösszem-javító + Refine Edge Élfinomítás + Relink to File Újracsatolás fájlhoz + Remove All Összes eltávolítása + Remove from Recent Eltávolítás a legutóbbiak közül + Remove Tool Eltávolító + Remove {name} from Recent Eltávolítás a legutóbbiak közül: {name} + Rename Layer Comp Rétegkompozíció átnevezése + Rendering mode Renderelési mód + Repeat Ismétlés + Replace Csere + Replace Contents Tartalom cseréje + Replace Footage Videoanyag cseréje + Resample Újramintavételezés + Reset Preferences Beállítások alaphelyzetbe + Resolution: Felbontás: + Restore Előző méret + Retry GPU GPU újrapróbálása + Retry GPU requires a restart. Save your work first. A GPU újrapróbálásához újraindítás kell. Előbb mentsd a munkádat. + Rgb RGB + Ridges Hullámgerincek + Right Jobbra + Rise Emelkedés + Rotate View Tool Nézetforgatás + Rotate view with trackpad Nézetforgatás érintőpárnával + Row Order Csatornasorrend + Ruler Tool Vonalzó + Same folder Ugyanaz a mappa + Sample a pixel anywhere on the screen Mintavétel a képernyő bármely pixeléből + Sampler Színmintavevő + Save in background Mentés a háttérben + Save your work and restart PhotoCraft to retry GPU acceleration. CPU rendering remains active for this session. A GPU-gyorsítás újrapróbálásához mentsd a munkádat, majd indítsd újra a PhotoCraftot. Ebben a munkamenetben a CPU-s megjelenítés marad aktív. + Saved a copy as {path} Másolat mentve: {path} + Saved {name} Mentve: {name} + Saved {path}; image assets: {n} in {dir} Mentve: {path}; képelemek: {n} ({dir}) + Scale Factor Méretezési tényező + Scale Units Méretarány mértékegysége + Scratch Disks Lapozólemezek + Scratch Sizes Lapozólemez-méretek + Scratch: {size} Lapozólemez: {size} + Screen resolution Képernyőfelbontás + Scroll to zoom Nagyítás görgetéssel + Scrubby slider acceleration Húzócsúszka gyorsítása + Seed Kezdőérték + Select Layer Réteg kijelölése + Selected Kijelölt + Selected at launch: {backend} Indításkor kiválasztva: {backend} + Set Colors Színek beállítása + Set Measurement Scale Mérési méretarány beállítása + Set shape fill type Alakzat kitöltéstípusának beállítása + Set shape stroke type Alakzat körvonaltípusának beállítása + Shape Alakzat + Sharpen for Élesítés célja + Shell Lower Alsó kagyló + Shell Upper Felső kagyló + Shift Edge Él eltolása + Shine Fénylés + shortcut billentyűparancs + Shortcut Billentyűparancs + Show canvas dots Vászonpontok megjelenítése + Show channels in color Csatornák megjelenítése színesen + Show crosshair in brush tip Szálkereszt az ecsethegyben + Show extension panels Bővítménypanelek megjelenítése + Show font names in english Betűnevek angolul + Show only crosshair while painting Festéskor csak szálkereszt + Show transformation values Alakítási értékek megjelenítése + Sides Oldalak + Sine Szinusz + Single Column Marquee Tool Egyoszlopos kijelölés + Single Row Marquee Tool Egysoros kijelölés + Slice Select Tool Szeletkijelölés + Slice Tool Szelet + Smart guide color Intelligens segédvonalak színe + Smart Radius Intelligens sugár + Snap vector tools and transforms to pixel grid Vektoreszközök és alakítások illesztése a pixelrácshoz + Snapshots Pillanatképek + Softness Lágyság + Software adapter Szoftveres adapter + Solarized Dark Solarized sötét + Solid Front Faces Tömör előlapok + Spin Pörgetés + Spot Healing Foltjavítás + Spot Healing Brush Tool Foltjavító ecset + Squareness Szögletesség + Squeeze Összenyomás + Stagger Lökésszerű + Standard Szabványos + Star Csillag + Star ratio Csillagarány + Start X Kezdő X + Start Y Kezdő Y + Step Backward Lépés vissza + Step {step} ({id}) failed: {error} {step}. lépés ({id}) sikertelen: {error} + Stop Leállítás + Stop playing/recording Lejátszás/rögzítés leállítása + Strength Erősség + Stretch Nyújtás + Stroke Path Útvonal körvonalazása + Stroke path with brush Útvonal körvonalazása ecsettel + Stroke Width Körvonal vastagsága + Stroke: Körvonal: + Studio Studio + Studio (Dark) Studio (sötét) + Studio (Light) Studio (világos) + Stylization Stilizálás + Switch Foreground and Background Colors Előtér- és háttérszín cseréje + Switched to CPU rendering Átváltás CPU-s megjelenítésre + System Rendszer + Tablet Tablet + Taper Elvékonyodás + Target Cél + Text file Szövegfájl + The canvas is shown as sRGB. Choose a monitor profile in Edit › Color Settings. A vászon sRGB-ként jelenik meg. Monitorprofilt itt választhatsz: Szerkesztés › Színbeállítások. + The canvas on {display} is shown as sRGB. Choose a monitor profile in Edit › Color Settings. A vászon sRGB-ként jelenik meg ezen a kijelzőn: {display}. Monitorprofilt itt választhatsz: Szerkesztés › Színbeállítások. + The GPU compositor wasn't used: {reason} A GPU-s képösszeállító nem volt használatban: {reason} + This redraw took {seconds} s on the CPU. Az újrarajzolás ideje a CPU-n: {seconds} s. + This session: {reason} Ebben a munkamenetben: {reason} + Tiny Apró + To Center Középre + To drop files, start PhotoCraft under XWayland: `{command}` Fájlok behúzásához indítsd a PhotoCraftot XWayland alatt: `{command}` + Tool Eszköz + Top Left Bal felső + Top Right Jobb felső + Touch gestures Érintési gesztusok + Transform Again on a Copy Alakítás ismétlése másolaton + Transition Átmenet + Transparency & Gamut Átlátszóság és színtartomány + Trap Ráfutás + Tree Fa + Triangle Tool Háromszög + Trim Levágás + Twist Csavarás + Unaltered Változatlan + Uniform Egyenletes + Units Mértékegységek + Units & Rulers Mértékegységek és vonalzók + Unknown Ismeretlen + unknown ismeretlen + unknown type ismeretlen típus + Unnamed {mode} profile Névtelen profil ({mode}) + Untagged {mode} Profil nélkül ({mode}) + Update Frissítés + Update Layer Comp Rétegkompozíció frissítése + Upper Felső + Use additional plugins folder További bővítménymappa használata + Use Custom Color Egyéni szín használata + Use File › Open, or paste a copied image with {paste}. Használd a Fájl › Megnyitás… parancsot, vagy illessz be egy másolt képet ({paste}). + Use gpu GPU használata + Use tablet pressure Tablet nyomásérzékelésének használata + Vanishing Point Enyészpont + Vertical Type Mask Tool Függőleges szövegmaszk + Vertical Type Tool Függőleges szöveg + Video Frames to Layers Videókockák rétegekbe + View Nézet + Wand Varázspálca + Warmth Melegség + Weight: Vastagság: + Window renderer: {renderer} Ablakmegjelenítő: {renderer} + Work Path Munkaútvonal + World ready Többnyelvű + Wrap Körbefordítás + X11 X11 + Zoom clicked point to center Kattintott pont nagyítása középre + Zoom resizes windows Nagyításkor az ablak is átméreteződik + Zoom with scroll wheel Nagyítás görgővel + Zoom with trackpad pinch Nagyítás csippentéssel az érintőpárnán + {name} options {name}: beállítások + {size} (was {old}) {size} (korábban: {old}) + {w} px x {h} px ({ppi} ppi) {w} px x {h} px ({ppi} ppi) + {w} {unit} x {h} {unit} ({ppi} ppi) {w} {unit} x {h} {unit} ({ppi} ppi) + …and {n} more …és még {n} + +# Labels kept for parity with the other catalogs + 10 fps 10 fps + 15 fps 15 fps + 24 fps 24 fps + 25 fps 25 fps + 250% 250% + 3D… 3D… + 85% 85% + 95% 95% + Accurate Pontos + ACES ACES + Actual Color Tényleges szín + Add a mask (from the selection; ⌥ inverts) Maszk hozzáadása (a kijelölésből; ⌥: invertálás) + Add Mask Maszk hozzáadása + Add Pins Tűk hozzáadása + Add to selection (⇧) Hozzáadás a kijelöléshez (⇧) + Adds grain to the soft edges of the brush tip. Szemcsézettséget ad az ecsethegy lágy széleihez. + Adjustment Layers Korrekciós rétegek + Adobe Every-line Composer Adobe többsoros szedőmotor + Adobe PDF Presets… Adobe PDF-előbeállítások… + Adobe Single-line Composer Adobe egysoros szedőmotor + Advanced Controls Speciális vezérlők + After (X) Utána (X) + Airbrush: keeps painting while the pointer rests. Festékszóró: a festés akkor is folytatódik, amikor a mutató áll. + Alternates for Current Selection Változatok a jelenlegi kijelöléshez + Aperture Rekesznyílás + Apple RGB Apple RGB + Apply to All Alkalmaz mindre + Are you sure you want to quit? Biztosan kilépsz? + Are you sure? Biztos vagy benne? + Arrangement Elrendezés + Art History Brush Művészi előzményecset + Ask Before Saving Saved Embedded Rákérdezés a beágyazott tartalom mentése előtt + Ask When Pasting Rákérdezés beillesztéskor + Audio Hang + Auto Clean Brush Ecset automatikus tisztítása + Auto-Hide Automatikus elrejtés + Auto-Leading Automatikus sorköz + Auto-leading % Automatikus sorköz % + Automatically Create First Snapshot Első pillanatkép automatikus létrehozása + Automatically Create New Snapshot When Saving Új pillanatkép automatikus létrehozása mentéskor + Avoid Widows/Orphans Fattyúsorok elkerülése + Blend RGB Colors Using Gamma 1.0 RGB-színek keverése 1,0 gammával + Blending Mode Keverési mód + Blue, Red, Yellow Kék, vörös, sárga + Browser storage is unavailable; brushes are session-only. Keep this tab open to retain them. A böngésző tárhelye nem érhető el, ezért az ecsetek csak ebben a munkamenetben maradnak meg. Ne zárd be ezt a lapot, ha meg akarod tartani őket. + Brush preset changes are not saved: {error} Az ecset-előbeállítások módosításai nincsenek mentve: {error} + Byline Szerző + Byline Title Szerző beosztása + Camera Make Fényképezőgép gyártója + Camera Model Fényképezőgép típusa + Canvas rotation Vászonforgatás + Caption Credits Képaláírás és forrás + Capture Comp Rétegkompozíció rögzítése + Changes have not been saved. A módosítások nincsenek mentve. + Changes Saved Módosítások mentve + Check for Updates Frissítések keresése + Choose Folder Mappa kiválasztása + Choose… Kiválasztás… + CIE RGB CIE RGB + City Város + Clear History Előzmények törlése + Click sets foreground · right-click sets background Kattintás: előtérszín · jobb kattintás: háttérszín + Click the first point or press ↵ to close · Esc cancels Lezáráshoz kattints az első pontra, vagy nyomd meg: ↵ · Esc: megszakítás + Click to add a point · drag off or ⌘/Ctrl-click or Delete to remove Kattintással pontot adhatsz hozzá · eltávolítás: lehúzás, ⌘/Ctrl-kattintás vagy Delete + Click to add text Kattints a szöveg hozzáadásához + Click to apply, ⌥-click to clear overrides, double-click for Style Options Kattintás: alkalmazás, ⌥-kattintás: felülbírálások törlése, dupla kattintás: stílusbeállítások + Click to zoom in · ⌥-click to zoom out Kattintás: nagyítás · ⌥-kattintás: kicsinyítés + Click: corner · Drag: smooth · Click first point: close · {key} finish · Esc cancel Kattintás: sarok · Húzás: sima · Kattintás az első pontra: lezárás · {key}: befejezés · Esc: megszakítás + Click: corner · Drag: smooth · Click first point: close · ↩ finish · Esc cancel Kattintás: sarok · Húzás: sima · Kattintás az első pontra: lezárás · ↩: befejezés · Esc: megszakítás + Coerce to sRGB Kényszerítés sRGB-re + Collapse group (⌥-click: all groups) Csoport összecsukása (⌥-kattintás: minden csoport) + Color Management Policies Színkezelési szabályok + Color Noise Reduction Színzaj csökkentése + ColorMatch RGB ColorMatch RGB + Commit any current edits (⌘↩) Folyamatban lévő szerkesztések véglegesítése (⌘↩) + Commit current crop operation (↵) Körbevágás véglegesítése (↵) + Commit Perspective Warp (↩) Perspektivikus torzítás véglegesítése (↩) + Commit Puppet Warp (↩) Bábtorzítás véglegesítése (↩) + Commit transform (↩) Alakítás véglegesítése (↩) + Commit warp (↩) Hajlítás véglegesítése (↩) + Convert Frames > Video Layers Képkockák átalakítása > Videórétegek + Convert to Grayscale Átalakítás szürkeárnyalatossá + Copper Réz + Copyright Info URL Szerzői jogi információk URL-je + Could not open Nem nyitható meg + Country Ország + Create a new layer (⇧⌘N) Új réteg létrehozása (⇧⌘N) + Create Date Létrehozás dátuma + Create Multiple Picture Packages Több képcsomag létrehozása + Create new count group Új számlálócsoport létrehozása + Create new set Új készlet létrehozása + Create Plane Sík létrehozása + Create Snapshot Pillanatkép létrehozása + Create… Létrehozás… + Credit Forrásmegjelölés + Current Time Aktuális idő + Custom pattern Egyéni mintázat + Date Created Létrehozás dátuma + DateTime Original Eredeti dátum és idő + Defaults restored. Alapértékek visszaállítva. + Delete Layer… Réteg törlése… + Delete Mask Maszk törlése + Delete Pins Tűk törlése + Delete Preset… Előbeállítás törlése… + Delete Smart Filters Intelligens szűrők törlése + Delete Snapshot Pillanatkép törlése + Delete… Törlés… + Desaturate Monitor Colors By Monitorszínek telítettségének csökkentése ennyivel + Detect Faces Arcfelismerés + Digits Számjegyek + Direction of Stroke Vonás iránya + Disable Mask Maszk letiltása + Discard Changes Módosítások elvetése + Do you want to save you made to “{name}” before quitting? Mented a módosításokat kilépés előtt? Dokumentum: „{name}” + Documentation Dokumentáció + Drag (lasso) or click points (polygonal) · {add} add · {sub} subtract Húzás (lasszó) vagy pontok kattintással (sokszög) · {add}: hozzáadás · {sub}: kivonás + Drag to move the layer Húzással mozgathatod a réteget + Duplicated selection name Kettőzött kijelölés neve + Edit Plane Sík szerkesztése + Edit… Szerkesztés… + Email E-mail + Embed profiles Profilok beágyazása + Enable Mask Maszk engedélyezése + Enable Smart Filters Intelligens szűrők engedélyezése + Enable Timeline Quick Config Idővonal gyorsbeállításának engedélyezése + Enter description… Írd be a leírást… + Error File Hibafájl + EXIF Version EXIF-verzió + Expand All Groups Összes csoport kibontása + Expand group (⌥-click: all groups) Csoport kibontása (⌥-kattintás: minden csoport) + Export Image Kép exportálása + Export to Exportálás célja + Exporting… Exportálás… + Exposure Program Expozíciós program + Failed to open file A fájl nem nyitható meg + Failed to save file A fájl nem menthető + Feather Radius Elmosott szél sugara + File Naming Fájlelnevezés + File not found A fájl nem található + File Size Fájlméret + Fill Light Derítőfény + First Color Readout Első színkijelzés + Fit to Window Ablakhoz igazítás + Flash Vaku + Flatten Frames Into Clips Képkockák egybesimítása klipekbe + Flatten Frames Into Layers Képkockák egybesimítása rétegekbe + Frame Duration Képkocka időtartama + Frame Sync Képkocka-szinkronizálás + General Brushes Általános ecsetek + Glyph Scaling Glifaméretezés + Go to Layer Ugrás a rétegre + GPS GPS + Gray: Szürke: + Grayscale Slider Szürkeárnyalat-csúszka + Grid (⌘') Rács (⌘') + Hanging Punctuation Függő írásjelek + HDTV (Rec. 709) HDTV (Rec. 709) + Heal Auto Javítás: automatikus + Heal Color Javítás: szín + Heal Off Javítás: ki + Heal On Javítás: be + Hide Grid Rács elrejtése + Hide the layer's effects (⌥-click: all layers) Rétegeffektusok elrejtése (⌥-kattintás: minden réteg) + HSB Color HSB-szín + HSL/Grayscale HSL/szürkeárnyalat + Humidity Páratartalom + Import Image Kép importálása + Import Options Importálási beállítások + Import Settings Beállítások importálása + Importing… Importálás… + Import… Importálás… + Include ICC Profile ICC-profil belefoglalása + Industry Standard Iparági szabvány + Info Options… Információ-beállítások… + Initial Direction of Stroke Vonás kezdőiránya + Input Levels Bemeneti szintek + Intersect with selection (⇧⌥) Metszet a kijelöléssel (⇧⌥) + Invert Selection Kijelölés megfordítása + Jag Cakkos + Keep proportions Méretarány megtartása + Keep this tab open to retain your brushes. Ne zárd be ezt a lapot, ha meg akarod tartani az ecseteidet. + Keep view in sync Nézetek szinkronban tartása + Keeps the current texture when switching presets. Előbeállítás váltásakor megtartja a jelenlegi textúrát. + Last Modified Utolsó módosítás + Layer Styles Rétegstílusok + Learn More További információ + Lens Corrections Objektívkorrekciók + Lens Info Objektívadatok + Limit Playback Lejátszás korlátozása + Live Preview Élő előnézet + Loading… Betöltés… + Localize Color Clusters Helyi színcsoportok + Lock Image Pixels Képpixelek zárolása + Lock Position Pozíció zárolása + Lock Transparent Pixels Átlátszó pixelek zárolása + Log Errors To File Hibák naplózása fájlba + Luminance Smoothness Luminancia simasága + Mac Crisp Mac tiszta + Mac LCD Mac LCD + Mac Sharp Mac éles + Mac Smooth Mac sima + Mac Strong Mac erős + Metering Mode Fénymérési mód + Minimalistic Minimalista + Missing Profiles: Ask When Opening Hiányzó profilok: rákérdezés megnyitáskor + Modify Date Módosítás dátuma + Mouse Controls Egérvezérlés + Mouse Coordinates Egérkoordináták + Native file drag-and-drop is not supported on Wayland yet. Use File › Open, or run PhotoCraft under XWayland with `WAYLAND_DISPLAY= photocraft`. A fájlok natív behúzása Waylanden még nem támogatott. Használd a Fájl › Megnyitás parancsot, vagy futtasd a PhotoCraftot XWaylanden: `WAYLAND_DISPLAY= photocraft`. + New channel only Csak új csatorna + New document… ⌘N Új dokumentum… ⌘N + New Layer from Brush Új réteg ecsetből + New Path Új útvonal + New Snapshot Új pillanatkép + New Video Group Új videócsoport + Newspaper Újságpapír + No Layer Comps Nincs rétegkompozíció + No Layers Nincs réteg + No recent files Nincs legutóbbi fájl + No Results Nincs találat + No Results Found Nincs találat + No Selection Nincs kijelölés + Nonlinear History Nemlineáris előzmények + Nothing to Redo Nincs mit újra végrehajtani + Nothing to Undo Nincs mit visszavonni + NTSC (1953) NTSC (1953) + Object Name / Title Objektum neve / cím + Opacity Mask Átlátszatlansági maszk + Open Image Kép megnyitása + Open… ⌘O Megnyitás… ⌘O + Operation canceled Művelet megszakítva + Operation cancelled Művelet megszakítva + Operation completed Művelet befejezve + Operation failed A művelet sikertelen + Overlay: Rule of Thirds Fedvény: harmadolási szabály + Override Action "Save" Commands A műveletsor „Mentés” parancsainak felülbírálása + Page Setup… Oldalbeállítás… + PAL/SECAM PAL/SECAM + Panel Options… Panelbeállítások… + Path to Illustrator… Útvonal Illustrator-fájlba… + PDF Presentation… PDF-bemutató… + Photo (2 × 3 in) Fotó (2 × 3 in) + Photo (4 × 6 in) Fotó (4 × 6 in) + Photo (5 × 7 in) Fotó (5 × 7 in) + Photo (8 × 10 in) Fotó (8 × 10 in) + Picture Package… Képcsomag… + Play Once Lejátszás egyszer + Please wait… Kis türelmet… + Preset Name: Előbeállítás neve: + Profile Mismatches: Ask When Opening Profileltérés: rákérdezés megnyitáskor + Province / State Megye / állam + Quick Sync Gyors szinkronizálás + Quit PhotoCraft Kilépés a PhotoCraftból + Recent Items Legutóbbi elemek + Reference point location Viszonyítási pont helye + Rendering… Renderelés… + Report a Bug Hiba bejelentése + Reset GPU Backend GPU-háttérrendszer alaphelyzetbe állítása + Restart Újraindítás + Restart Required Újraindítás szükséges + Restore last viewed comp Utoljára megtekintett rétegkompozíció visszaállítása + Retry saving presets Előbeállítások mentésének újrapróbálása + Roman Hanging Punctuation Latin függő írásjelek + Rotate Auto Forgatás: automatikus + Rotate Fixed Forgatás: rögzített + Ruler Units Vonalzó mértékegysége + Rulers (⌘R) Vonalzók (⌘R) + Save before closing? Mented bezárás előtt? + Save Changes Módosítások mentése + Save Image Kép mentése + Saved Multichannel Többcsatornásként mentve + Saving brush presets… Keep this tab open until saving finishes. Ecset-előbeállítások mentése… Ne zárd be ezt a lapot, amíg a mentés tart. + Saving… Mentés… + Scale Horizontally Vízszintes méretezés + Scale Vertically Függőleges méretezés + Search commands (⌘K) Parancsok keresése (⌘K) + Second Color Readout Második színkijelzés + Select Folder… Mappa kiválasztása… + Select Plane Sík kijelölése + Select Similar Colors Hasonló színek kijelölése + Set New Snapshot as Default Új pillanatkép beállítása alapértelmezettként + Set Start Timecode… Kezdő időkód beállítása… + Show Guides (⌘;) Segédvonalak megjelenítése (⌘;) + Show New Snapshot Dialog by Default Új pillanatkép ablakának megjelenítése alapértelmezés szerint + Show the layer's effects (⌥-click: all layers) Rétegeffektusok megjelenítése (⌥-kattintás: minden réteg) + Shutter Speed Záridő + Simple Egyszerű + Single Word Justification Egyszavas sorkizárás + Smart Object Layer Intelligensobjektum-réteg + SMPTE C SMPTE C + Snap to Grid Illesztés a rácshoz + Snapshot 1 Pillanatkép 1 + Some brush presets could not be loaded Egyes ecset-előbeállítások nem tölthetők be + Spacing: Térköz: + Split at Playhead Felosztás a lejátszófejnél + Split Toning Osztott tónusozás + Status Information Állapotinformáció + Step Forward Lépés előre + Stop For Errors Leállás hiba esetén + Subdivisions Felosztások + Subject Distance Tárgytávolság + Subscript Alsó index + Subtract from selection (⌥) Kivonás a kijelölésből (⌥) + Superscript Felső index + Supplemental Categories Kiegészítő kategóriák + Switch theme Témaváltás + Technology Previews Technológiai előzetesek + Technology Previews… Technológiai előzetesek… + The current gradient (pick one in Window › Gradients) A jelenlegi színátmenet (másikat az Ablak › Színátmenetek panelen választhatsz) + The file could not be opened. A fájl nem nyitható meg. + The file could not be saved. A fájl nem menthető. + Timecode Időkód + Toggle reference point Viszonyítási pont be/ki + Toolbars Eszköztárak + Total Ink Teljes festékmennyiség + Transmission Reference Továbbítási azonosító + Turbulent Turbulens + UI font size Felület betűmérete + Ultra Bold Extrakövér + Ultra Light Extravékony + Uncoated CMYK Bevonat nélküli CMYK + Unlocked Zárolás feloldva + Unsaved Changes Nem mentett módosítások + Unsupported file format Nem támogatott fájlformátum + Unsupported format Nem támogatott formátum + Urgency Sürgősség + Use File › Open to open an image. Kép megnyitásához használd a Fájl › Megnyitás parancsot. + Uses the foreground and background colors. Az előtér- és a háttérszínt használja. + Violet, Orange Ibolya, narancs + Watercolour look: lighter interior, darker rims. Akvarellhatás: világosabb belső rész, sötétebb szélek. + Web Photo Gallery… Webes fotógaléria… + WebP (lossless) WebP (veszteségmentes) + Wide Gamut RGB Wide Gamut RGB + Word Spacing Szótávolság + Working Gray Plate Szürke munkalemez + Working Space Munkaszíntér + Working Spaces Munkaszínterek + {path}: the path is not valid Unicode; rename the file and open it again. {path}: az elérési út nem érvényes Unicode-szöveg. Nevezd át a fájlt, és nyisd meg újra. + {zoom}% {zoom}% + ↑↓ navigate ↵ run esc close ↑↓ léptetés ↵ futtatás esc bezárás + Amount: Mérték: + Apply Vanishing Point ({key}) Enyészpont alkalmazása ({key}) + Apply the removal ({key}) Eltávolítás alkalmazása ({key}) + Drag to draw the curve · {key}-click draws a straight line Húzással rajzolja meg a görbét · {key}-kattintás egyenes vonalat húz + Draw to modify the curve Rajzoljon a görbe módosításához + Edit points to modify the curve Szerkessze a pontokat a görbe módosításához + Fix the red plane first: its corners must form a convex quad Először javítsa a piros síkot: sarkainak konvex négyszöget kell alkotniuk + Grid Size: Rácsméret: + Light Blue Világoskék + Light Red Világospiros + Medium Blue Középkék + Merge Group Csoport egyesítése + Protect skin tones Bőrtónusok védelme + Protect: Védelem: + Remove after each stroke Eltávolítás minden ecsetvonás után + Saving {percent} Mentés: {percent} + Scale or rotate the moved area, then press {key} Méretezze vagy forgassa el az áthelyezett területet, majd nyomja meg: {key} + Smooth the curve values A görbeértékek simítása + Strokes to remove: {n} (Enter applies, Esc cancels) Eltávolítandó ecsetvonások: {n} (Enter: alkalmazás, Esc: mégse) + The layer or its mask changed. Drag the mask onto the Trash again. A réteg vagy a maszkja megváltozott. Húzza újra a maszkot a Kukába. + Transform On Drop Átalakítás elengedéskor + Vanishing Point is not active Az Enyészpont nem aktív + Vanishing Point: click four corners to create a plane Enyészpont: sík létrehozásához kattintson a négy sarokra diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index be8cb5c15e..fe6e8e680d 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -39,7 +39,7 @@ pub struct LangInfo { /// Catalog file contents, raw-deflated by build.rs (empty for the built-in English); see /// [`LangInfo::text`]. pub source: &'static [u8], - /// Plural form index for a count (English: 0 = one, 1 = other; Japanese and Chinese: always 0; + /// Plural form index for a count (English: 0 = one, 1 = other; Japanese, Chinese and Hungarian: always 0; /// Czech: 0 = one, 1 = few (2–4), 2 = other; French: 0 = one (0 and 1), 1 = other). A catalog's `@plural` entries list one form per /// index. pub plural: fn(u64) -> usize, @@ -141,7 +141,7 @@ fn inflate(source: &[u8]) -> Result { } /// The registry. English first: it is the fallback and the source language. -pub static LANGUAGES: [LangInfo; 19] = [ +pub static LANGUAGES: [LangInfo; 20] = [ LangInfo { code: "en", name: "English", source: b"", plural: plural_one_other, complete_menus: false, catalog: OnceLock::new() }, LangInfo { code: "ja", name: "日本語", source: catalog!("ja"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, LangInfo { code: "zh-hans", name: "简体中文", source: catalog!("zh-hans"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, @@ -164,6 +164,8 @@ pub static LANGUAGES: [LangInfo; 19] = [ // Dutch; `nl-NL` and `nl-BE` locales both resolve here. LangInfo { code: "nl", name: "Nederlands", source: catalog!("nl"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() }, LangInfo { code: "it", name: "Italiano", source: catalog!("it"), plural: plural_one_other, complete_menus: true, catalog: OnceLock::new() }, + // Hungarian: a count takes the singular noun ("3 réteg"), so one plural form. + LangInfo { code: "hu", name: "Magyar", source: catalog!("hu"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, // Turkish; `tr`, `tr-TR` and `tr_TR` locales resolve here. LangInfo { code: "tr", name: "Türkçe", source: catalog!("tr"), plural: plural_none, complete_menus: true, catalog: OnceLock::new() }, // Vietnamese; `vi-VN` resolves here. @@ -420,6 +422,8 @@ mod tests { assert_eq!(lang_from_tag("it-CH"), Lang::from_code("it")); assert_eq!(lang_from_tag("nl_NL.UTF-8"), Lang::from_code("nl")); assert_eq!(lang_from_tag("nl-BE"), Lang::from_code("nl")); + assert_eq!(lang_from_tag("hu_HU.UTF-8"), Lang::from_code("hu")); + assert_eq!(lang_from_tag("hu-HU"), Lang::from_code("hu")); assert_eq!(lang_from_tag("tr_TR.UTF-8"), Lang::from_code("tr")); assert_eq!(lang_from_tag("tr-TR"), Lang::from_code("tr")); assert_eq!(lang_from_tag("tr"), Lang::from_code("tr")); @@ -518,6 +522,22 @@ mod tests { assert_eq!(trn(es, 3, "{n} item", "{n} items"), "3 elementos"); } + #[test] + fn hungarian_resolves_and_keeps_the_singular_after_counts() { + let hu = Lang::from_code("hu").expect("hu registered"); + for tag in ["hu", "hu_HU.UTF-8", "hu-HU"] { + assert_eq!(lang_from_tag(tag), Some(hu), "{tag}"); + } + assert!(hu.complete_menus()); + assert_eq!(hu.name(), "Magyar"); + assert_eq!(tr(hu, "Layer"), "Réteg"); + for (n, expected) in [(0, "0 réteg"), (1, "1 réteg"), (3, "3 réteg"), (21, "21 réteg")] { + assert_eq!(trn(hu, n, "{n} layer", "{n} layers"), expected); + } + assert_eq!(tr_ctx(hu, "layerLabel", "Red"), "Piros"); + assert_eq!(tr(hu, "Red"), "Vörös"); + } + #[test] fn brazilian_portuguese_resolves_and_pluralises() { let pt = Lang::from_code("pt-br").expect("pt-br registered"); diff --git a/crates/ui-egui/src/i18n/system.rs b/crates/ui-egui/src/i18n/system.rs index 68afea8d85..0b9118386a 100644 --- a/crates/ui-egui/src/i18n/system.rs +++ b/crates/ui-egui/src/i18n/system.rs @@ -96,6 +96,7 @@ mod tests { (vec!["pt-PT"], "pt-br"), (vec!["it-IT", "en-US"], "it"), (vec!["nl-BE", "fr-BE"], "nl"), + (vec!["hu-HU", "en-US"], "hu"), (vec!["tr-TR", "en-US"], "tr"), (vec![], "en"), ] { diff --git a/crates/ui-egui/tests/crop_default_mode.rs b/crates/ui-egui/tests/crop_default_mode.rs index 19e885a967..b16e628214 100644 --- a/crates/ui-egui/tests/crop_default_mode.rs +++ b/crates/ui-egui/tests/crop_default_mode.rs @@ -24,6 +24,8 @@ fn harness() -> Option { egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 640.0)).with_max_steps(64).wgpu().build_eframe(|cc| { PhotocraftApp::setup_context(&cc.egui_ctx, Default::default()); let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), Default::default()); + // The queries below use English labels; don't follow the system locale. + app.session.prefs.edit(|p| p.interface.language = "en".into()); if let Some(rs) = cc.wgpu_render_state.as_ref() { app.set_wgpu(rs.clone()); } diff --git a/docs/localization.md b/docs/localization.md index 31737bf7a8..f05b261b36 100644 --- a/docs/localization.md +++ b/docs/localization.md @@ -25,6 +25,7 @@ contextual, command-ID and plural translations; missing entries fall back to Eng | Polski | `pl` | one / few (2–4, not 12–14) / many | | Ελληνικά | `el` | one / other | | Nederlands | `nl` | one / other | +| Magyar | `hu` | one form | | Tiếng Việt | `vi` | one form | Languages registered with `complete_menus` cover the current menu labels, `tl!` literals, blend diff --git a/xtask/src/i18n_coverage.rs b/xtask/src/i18n_coverage.rs index 2be43cb538..1cb64f1782 100644 --- a/xtask/src/i18n_coverage.rs +++ b/xtask/src/i18n_coverage.rs @@ -579,6 +579,7 @@ mod tests { ("el", "el.tsv"), ("uk", "uk.tsv"), ("nl", "nl.tsv"), + ("hu", "hu.tsv"), ("tr", "tr.tsv"), ("vi", "vi.tsv"), ] { From 3d6050b9318f27fd00e44c4b6813837b1a2ae7d2 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 11:08:34 -0400 Subject: [PATCH 24/27] Release workflow: pass the Azure signing secrets so Windows builds are signed (#3794) Since the release workflow moved to storytold/craft-common-actions, it has passed only the Apple and WINDOWS_CERTIFICATE secrets. The signing job reads values from the release environment, but only for names the caller lists, so the AZURE_* names that Trusted Signing uses came through empty and Windows builds would ship unsigned. Same fix as storytold/craft-launcher#174. Co-authored-by: Claude Opus 5.5 --- .github/workflows/release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85d0cd2c81..bfdca9e3e9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,3 +35,9 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} + AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} + AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} From 2db6b7d8f9ddb5be8d29f61b918133b22e41c5bc Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 11:11:02 -0400 Subject: [PATCH 25/27] Release 0.8.1: rebuild with the fixed shared release workflow The previous release build failed in craft-common-actions, not in this repo: riscv64 packaging got --formats deb,rpm,tar (fixed in craft-common-actions#3), Linux jobs lacked the ALSA headers (craft-common-actions#5), and Windows signing needed the AZURE_* secrets listed in the caller. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 58 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f17edd1171..be740d0ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4183,7 +4183,7 @@ dependencies = [ [[package]] name = "photocraft" -version = "0.8.0" +version = "0.8.1" dependencies = [ "arboard", "async-io", @@ -4225,7 +4225,7 @@ dependencies = [ [[package]] name = "photocraft-affinity" -version = "0.8.0" +version = "0.8.1" dependencies = [ "crc32fast", "flate2", @@ -4235,7 +4235,7 @@ dependencies = [ [[package]] name = "photocraft-algo" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "photocraft-automation" -version = "0.8.0" +version = "0.8.1" dependencies = [ "base64", "cap-std", @@ -4270,7 +4270,7 @@ dependencies = [ [[package]] name = "photocraft-cli" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-automation", "photocraft-codecs", @@ -4284,7 +4284,7 @@ dependencies = [ [[package]] name = "photocraft-cms" -version = "0.8.0" +version = "0.8.1" dependencies = [ "moxcms", "rayon", @@ -4292,7 +4292,7 @@ dependencies = [ [[package]] name = "photocraft-codecs" -version = "0.8.0" +version = "0.8.1" dependencies = [ "exr", "flate2", @@ -4313,7 +4313,7 @@ dependencies = [ [[package]] name = "photocraft-color" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-cms", "serde", @@ -4321,7 +4321,7 @@ dependencies = [ [[package]] name = "photocraft-compose" -version = "0.8.0" +version = "0.8.1" dependencies = [ "criterion", "photocraft-color", @@ -4336,7 +4336,7 @@ dependencies = [ [[package]] name = "photocraft-doc" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4347,7 +4347,7 @@ dependencies = [ [[package]] name = "photocraft-engine" -version = "0.8.0" +version = "0.8.1" dependencies = [ "blake3", "exr", @@ -4381,7 +4381,7 @@ dependencies = [ [[package]] name = "photocraft-format" -version = "0.8.0" +version = "0.8.1" dependencies = [ "blake3", "crc32fast", @@ -4401,14 +4401,14 @@ dependencies = [ [[package]] name = "photocraft-geom" -version = "0.8.0" +version = "0.8.1" dependencies = [ "serde", ] [[package]] name = "photocraft-gpu" -version = "0.8.0" +version = "0.8.1" dependencies = [ "half", "log", @@ -4425,14 +4425,14 @@ dependencies = [ [[package]] name = "photocraft-heif" -version = "0.8.0" +version = "0.8.1" dependencies = [ "heic-rs", ] [[package]] name = "photocraft-io" -version = "0.8.0" +version = "0.8.1" dependencies = [ "blake3", "crc32fast", @@ -4461,14 +4461,14 @@ dependencies = [ [[package]] name = "photocraft-ops" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-doc", ] [[package]] name = "photocraft-paint" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4479,7 +4479,7 @@ dependencies = [ [[package]] name = "photocraft-plugins" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4494,7 +4494,7 @@ dependencies = [ [[package]] name = "photocraft-psd" -version = "0.8.0" +version = "0.8.1" dependencies = [ "flate2", "photocraft-psd", @@ -4505,7 +4505,7 @@ dependencies = [ [[package]] name = "photocraft-raster" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4515,7 +4515,7 @@ dependencies = [ [[package]] name = "photocraft-raw" -version = "0.8.0" +version = "0.8.1" dependencies = [ "flate2", "image", @@ -4530,7 +4530,7 @@ dependencies = [ [[package]] name = "photocraft-tablet" -version = "0.8.0" +version = "0.8.1" dependencies = [ "block2 0.6.2", "objc2 0.6.4", @@ -4542,7 +4542,7 @@ dependencies = [ [[package]] name = "photocraft-testkit" -version = "0.8.0" +version = "0.8.1" dependencies = [ "memory-stats", "photocraft-color", @@ -4553,7 +4553,7 @@ dependencies = [ [[package]] name = "photocraft-text" -version = "0.8.0" +version = "0.8.1" dependencies = [ "flate2", "parley", @@ -4567,7 +4567,7 @@ dependencies = [ [[package]] name = "photocraft-ui-egui" -version = "0.8.0" +version = "0.8.1" dependencies = [ "base64", "craft-ui", @@ -4606,7 +4606,7 @@ dependencies = [ [[package]] name = "photocraft-vector" -version = "0.8.0" +version = "0.8.1" dependencies = [ "photocraft-color", "photocraft-doc", @@ -4617,7 +4617,7 @@ dependencies = [ [[package]] name = "photocraft-web" -version = "0.8.0" +version = "0.8.1" dependencies = [ "eframe", "egui", @@ -7450,7 +7450,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.8.0" +version = "0.8.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 87e0a378ce..10bbfc3b4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/photocraft", "apps/photocraft-cli", "xtask"] [workspace.package] -version = "0.8.0" +version = "0.8.1" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" From 184c226a76c9ca6234d473912d438feaa48c1cb0 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 12 Oct 2026 11:48:28 -0400 Subject: [PATCH 26/27] Fix main: wasm size cap, Windows Export As test, filter preview window (scope and a crash), CLI duplicate font warnings (#4147) * test(ui-egui): Export As artboards test accepts the Windows path separator The test added in #3909 compared the second file's path with a literal '/', but the exporter joins the sibling name with the platform separator, so Windows CI saw '/out\boards-2-Capsule.jpg' and failed. Co-Authored-By: Claude Opus 5.5 * fix(ui): only Filter menu dialogs get the in-dialog preview window #3877 drew the preview window in every schema dialog with a Preview checkbox, including Image > Rotation > Arbitrary. Photoshop's Rotate Canvas and adjustment dialogs have no preview window, and the taller dialog covered the canvas, which broke arbitrary_rotation_preview::size_changing_rotation_preview_is_visible_and_preserves_the_panned_center on main. The window and its reserved height now apply to filter.* commands only. Co-Authored-By: Claude Opus 5.5 * build(web): opt-level z for large cold crates in the wasm release profile The browser wasm sat at the 24 MiB gate. The document model, file parsers (psd, format, affinity), serde_json, usvg and naga are off the per-frame and per-pixel paths; opt-level "z" for them takes the release wasm from 25,186,233 to 24,829,840 bytes (-356 KB) with no behaviour change. Co-Authored-By: Claude Opus 5.5 * fix(cli): run prints each missing-font warning once Since #3630 the headless open and save also report missing fonts, so `run --out` and `run --cmd ... --out` printed the same warning two or three times and missing_typeface_is_reported_by_run_info_and_convert failed on main. Warnings from the open and the save now go through the same already-reported set as the per-command ones. Co-Authored-By: Claude Opus 5.5 * fix(ui): the filter preview window never uploads a texture past the GPU limit With low-resolution previews off, the preview window from #3877 uploaded the whole document as one texture. egui panics on a texture bigger than max_texture_side, so opening a filter dialog on a large image crashed (full_res_preview::filter_dialog_previews_at_full_resolution_when_low_res_is_off). The texture is now reduced to fit; the view maps through UV fractions, so it still lines up. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- Cargo.toml | 18 +++++++++ apps/photocraft-cli/src/lib.rs | 9 ++++- crates/ui-egui/src/file_dialog/tests.rs | 4 +- crates/ui-egui/src/filter_preview_ui.rs | 52 ++++++++++++++++++++++++- docs/development.md | 2 +- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 10bbfc3b4a..6b13954fd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,24 @@ opt-level = "z" [profile.wasm-release.package.photocraft-web] opt-level = "z" +# Large crates off the per-frame and per-pixel paths: the document model, file parsers run once +# per open/save, JSON (settings, presets, control channel), SVG parsing, and shader translation +# (runs when a pipeline is created). "z" keeps the wasm well under the size gate. +[profile.wasm-release.package.photocraft-doc] +opt-level = "z" +[profile.wasm-release.package.photocraft-psd] +opt-level = "z" +[profile.wasm-release.package.photocraft-format] +opt-level = "z" +[profile.wasm-release.package.photocraft-affinity] +opt-level = "z" +[profile.wasm-release.package.serde_json] +opt-level = "z" +[profile.wasm-release.package.usvg] +opt-level = "z" +[profile.wasm-release.package.naga] +opt-level = "z" + [profile.wasm-release.package.photocraft-raster] opt-level = 3 [profile.wasm-release.package.photocraft-color] diff --git a/apps/photocraft-cli/src/lib.rs b/apps/photocraft-cli/src/lib.rs index 560eaf71fc..93090610a5 100644 --- a/apps/photocraft-cli/src/lib.rs +++ b/apps/photocraft-cli/src/lib.rs @@ -292,11 +292,15 @@ fn command_list(a: &Args) -> Result, String> { fn run_cmds(a: &Args, out: &mut dyn Write, err: &mut dyn Write) -> R { let opts = export_opts(a)?; let mut h = Headless::trusted_local(); + // Each missing-font warning is printed once, wherever it is first detected (open, a command or + // the save). + let mut reported_fonts = std::collections::HashSet::::new(); match (a.positional.as_slice(), a.get("--new")) { ([file], None) => { let o = h.open(Path::new(file)).map_err(|e| e.to_string())?; let ws: Vec = serde_json::from_value(o["warnings"].clone()).unwrap_or_default(); warn_all(err, &ws); + reported_fonts.extend(ws); } ([], Some(new)) => { let p: Value = serde_json::from_str(new).map_err(|e| format!("bad --new JSON: {e}"))?; @@ -308,7 +312,6 @@ fn run_cmds(a: &Args, out: &mut dyn Write, err: &mut dyn Write) -> R { if cmds.is_empty() && a.get("--out").is_none() { return Err("run needs at least one --cmd (or --out)".into()); } - let mut reported_fonts = std::collections::HashSet::::new(); for (id, params) in cmds { let r = h.command_run(&id, params).map_err(|e| format!("`{id}`: {e}"))?; if let Some(e) = photocraft_engine::actions_cmds::nested_failure(&id, &r) { @@ -341,7 +344,9 @@ fn run_cmds(a: &Args, out: &mut dyn Write, err: &mut dyn Write) -> R { warn_all(err, &remaining); if let Some(o) = a.get("--out") { let r = h.save(None, Some(Path::new(o)), a.get("--format"), &opts).map_err(|e| e.to_string())?; - let ws: Vec = serde_json::from_value(r["warnings"].clone()).unwrap_or_default(); + // The save repeats the document's missing-font warnings; each was already reported above. + let ws: Vec = + serde_json::from_value::>(r["warnings"].clone()).unwrap_or_default().into_iter().filter(|w| !reported_fonts.contains(w)).collect(); warn_all(err, &ws); } Ok(()) diff --git a/crates/ui-egui/src/file_dialog/tests.rs b/crates/ui-egui/src/file_dialog/tests.rs index 3081de03f7..202cc73888 100644 --- a/crates/ui-egui/src/file_dialog/tests.rs +++ b/crates/ui-egui/src/file_dialog/tests.rs @@ -719,7 +719,9 @@ fn export_as_with_several_artboards_selected_writes_one_file_each() { assert!(!app.ui.status_error, "{}", app.ui.status); assert_eq!(written.borrow().len(), 2); assert_eq!(written.borrow()[0], "/out/boards.jpg"); - assert!(written.borrow()[1].starts_with("/out/boards-2-") && written.borrow()[1].ends_with(".jpg"), "{:?}", written.borrow()); + // The sibling path is joined with the platform separator (`/out\boards-2-…` on Windows). + let second = written.borrow()[1].replace('\\', "/"); + assert!(second.starts_with("/out/boards-2-") && second.ends_with(".jpg"), "{:?}", written.borrow()); let mut got = sizes.borrow().iter().map(|(_, w, h)| (*w, *h)).collect::>(); got.sort_unstable(); assert_eq!(got, [(10, 25), (20, 15)], "each board at its own size, scaled to 50 %"); diff --git a/crates/ui-egui/src/filter_preview_ui.rs b/crates/ui-egui/src/filter_preview_ui.rs index 5f4ac13924..bf2cb47b58 100644 --- a/crates/ui-egui/src/filter_preview_ui.rs +++ b/crates/ui-egui/src/filter_preview_ui.rs @@ -30,7 +30,7 @@ const CENTER_Y_FIELD: &str = "__pvCy"; /// Draw the filter dialog's in-dialog preview window below its parameters (its `fields` `f`). A /// no-op for dialogs without a preview and for Select and Mask, which has its own view. pub(crate) fn window(app: &PhotocraftApp, dialog: u64, ui: &mut egui::Ui, f: &mut Map) { - if !f.contains_key("__preview") || crate::select_mask_view::owns(f) { + if !shows(f) { return; } let Some(cmd) = f.get("__command").and_then(Value::as_str).map(str::to_owned) else { return }; @@ -145,7 +145,14 @@ const ZOOM_STEPS: [f32; 7] = [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0]; /// The vertical space the preview window takes below the parameters: the image, the zoom row and /// their spacing. `0` when the dialog has no preview (so the caller reserves nothing). pub(crate) fn reserved_height(f: &Map) -> f32 { - if f.contains_key("__preview") && !crate::select_mask_view::owns(f) { PREVIEW_H + 32.0 } else { 0.0 } + if shows(f) { PREVIEW_H + 32.0 } else { 0.0 } +} + +/// Whether the dialog with fields `f` gets the preview window: Filter menu dialogs with a Preview +/// checkbox, as in Photoshop. Image › Rotation › Arbitrary and the adjustment dialogs keep only +/// the on-canvas preview, and Select and Mask has its own view. +fn shows(f: &Map) -> bool { + f.contains_key("__preview") && !crate::select_mask_view::owns(f) && f.get("__command").and_then(Value::as_str).is_some_and(|c| c.starts_with("filter.")) } /// The next zoom level up or down from `zoom`, clamped to [`ZOOM_STEPS`]' ends. @@ -194,12 +201,29 @@ fn document_size(proxy: [u32; 2], k: u32) -> Vec2 { /// A composite as monitor pixels, matching the canvas (`canvas::display_image`). fn upload(ctx: &egui::Context, app: &PhotocraftApp, name: &str, doc: &Document) -> egui::TextureHandle { + // Never exceed what the GPU accepts (egui panics on an oversized texture): with low-resolution + // previews off the proxy is the full document. The view maps through UV fractions, so a reduced + // texture still lines up. + let k = texture_factor(doc.size.width, doc.size.height, ctx.input(|i| i.max_texture_side)); + let reduced; + let doc = if k > 1 { + reduced = crate::proxy::proxy_document(doc, k); + &reduced + } else { + doc + }; let buf = photocraft_compose::flatten(doc); let (display, _) = crate::canvas::canvas_display(app, doc, None); let img = crate::canvas::display_image(display.as_deref(), &buf); ctx.load_texture(name, img, egui::TextureOptions::LINEAR) } +/// The reduction that fits a `w`×`h` image inside a `max_side` texture (1: it already fits). +fn texture_factor(w: u32, h: u32, max_side: usize) -> u32 { + let max_side = u32::try_from(max_side).unwrap_or(u32::MAX).max(1); + w.max(h).div_ceil(max_side).max(1) +} + /// What a cached texture was built from, so it is rebuilt only when its source changes. #[derive(Clone, PartialEq)] enum TexKey { @@ -238,6 +262,30 @@ mod tests { assert_eq!(step_zoom(f32::NAN, false), MIN_ZOOM); } + #[test] + fn only_filter_dialogs_get_the_preview_window() { + let fields = |cmd: &str| json!({"__preview": true, "__command": cmd}).as_object().cloned().unwrap(); + assert!(shows(&fields("filter.blur.gaussianBlur"))); + assert!(reserved_height(&fields("filter.blur.gaussianBlur")) > 0.0); + // Photoshop's Rotate Canvas and adjustment dialogs have no preview window. + assert!(!shows(&fields("image.rotation.arbitrary"))); + assert_eq!(reserved_height(&fields("image.rotation.arbitrary")), 0.0); + assert!(!shows(&fields("adjust.levels"))); + // No Preview checkbox or no command: no window. + assert!(!shows(json!({"__command": "filter.blur.gaussianBlur"}).as_object().unwrap())); + assert!(!shows(json!({"__preview": true}).as_object().unwrap())); + } + + #[test] + fn textures_are_reduced_to_fit_the_gpu() { + assert_eq!(texture_factor(3000, 2000, 2048), 2); + assert_eq!(texture_factor(2048, 100, 2048), 1); + assert_eq!(texture_factor(9000, 9000, 2048), 5); + // A zero or absurd limit never divides by zero. + assert_eq!(texture_factor(3000, 2000, 0), 3000); + assert_eq!(texture_factor(0, 0, 2048), 1); + } + #[test] fn the_centre_is_clamped_to_the_document() { assert_eq!(clamp_center(-5.0, 99.0, 100.0, 50.0), (0.0, 50.0)); diff --git a/docs/development.md b/docs/development.md index 1236dd65c5..74b61de1d5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -352,7 +352,7 @@ trunk build --release # writes ../../dist/web (index.html, .js glue trunk serve --release # dev server on http://127.0.0.1:8765 ``` -Any static file server works for `dist/web`, for example `python3 -m http.server 8765` run inside that directory. Trunk downloads the matching `wasm-bindgen` and `wasm-opt` itself. `trunk build --release` uses the `wasm-release` Cargo profile (`data-cargo-profile` in `index.html`: fat LTO, opt-level "z" for `photocraft-ui-egui`, "s" for other crates except pixel processing, which keeps level 3). The UI override reduces browser code size while retaining the speed-oriented settings for filters and compositing. Serve the `.wasm` with compression; its raw size varies with the source and compiler. Keep it under 24 MiB (`packaging/web/package.sh` enforces this; Cloudflare's per-file cap is 25 MiB). Pull-request CI also builds the actual browser app with the same pinned Trunk version as the release job and runs `packaging/web/package.sh`, including the relative-URL and 24 MiB checks. The `web build and wasm size gate` check runs on main and on PRs that change something the web build compiles or packages (`apps/photocraft-web`, `packaging/web`, crate sources and manifests, `Cargo.toml`/`Cargo.lock`, the CI workflow), and reports the resulting wasm size in the Actions job summary. The web build never embeds craft-fonts (see Fonts above). To see where the bytes go, run `twiggy top -n 40` on `target/wasm32-unknown-unknown/wasm-release/photocraft-web.wasm` (before wasm-opt strips the names). +Any static file server works for `dist/web`, for example `python3 -m http.server 8765` run inside that directory. Trunk downloads the matching `wasm-bindgen` and `wasm-opt` itself. `trunk build --release` uses the `wasm-release` Cargo profile (`data-cargo-profile` in `index.html`: fat LTO, opt-level "z" for the UI, engine, I/O and web shells and for large crates off the hot paths (document model, file parsers, JSON, SVG parsing, shader translation), "s" for other crates except pixel processing, which keeps level 3). The UI override reduces browser code size while retaining the speed-oriented settings for filters and compositing. Serve the `.wasm` with compression; its raw size varies with the source and compiler. Keep it under 24 MiB (`packaging/web/package.sh` enforces this; Cloudflare's per-file cap is 25 MiB). Pull-request CI also builds the actual browser app with the same pinned Trunk version as the release job and runs `packaging/web/package.sh`, including the relative-URL and 24 MiB checks. The `web build and wasm size gate` check runs on main and on PRs that change something the web build compiles or packages (`apps/photocraft-web`, `packaging/web`, crate sources and manifests, `Cargo.toml`/`Cargo.lock`, the CI workflow), and reports the resulting wasm size in the Actions job summary. The web build never embeds craft-fonts (see Fonts above). To see where the bytes go, run `twiggy top -n 40` on `target/wasm32-unknown-unknown/wasm-release/photocraft-web.wasm` (before wasm-opt strips the names). URL flags: `?webgl` forces the WebGL2 backend, and `?cpu` forces the CPU canvas path. From d43db3e153c76aefce9b32cef9099e458eaf17a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 12:57:00 -0400 Subject: [PATCH 27/27] Release 0.8.2: web build back under the wasm size gate 0.8.1's Web job failed: photocraft-web's wasm was 25,587,059 bytes, over packaging/web/package.sh's 24 MiB gate. #4253 merged with #4147 brings it to 24,161,781. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 58 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bcedc1ace..ec1ddff119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4183,7 +4183,7 @@ dependencies = [ [[package]] name = "photocraft" -version = "0.8.1" +version = "0.8.2" dependencies = [ "arboard", "async-io", @@ -4225,7 +4225,7 @@ dependencies = [ [[package]] name = "photocraft-affinity" -version = "0.8.1" +version = "0.8.2" dependencies = [ "crc32fast", "flate2", @@ -4235,7 +4235,7 @@ dependencies = [ [[package]] name = "photocraft-algo" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4248,7 +4248,7 @@ dependencies = [ [[package]] name = "photocraft-automation" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "cap-std", @@ -4270,7 +4270,7 @@ dependencies = [ [[package]] name = "photocraft-cli" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-automation", "photocraft-codecs", @@ -4284,7 +4284,7 @@ dependencies = [ [[package]] name = "photocraft-cms" -version = "0.8.1" +version = "0.8.2" dependencies = [ "moxcms", "rayon", @@ -4292,7 +4292,7 @@ dependencies = [ [[package]] name = "photocraft-codecs" -version = "0.8.1" +version = "0.8.2" dependencies = [ "exr", "flate2", @@ -4313,7 +4313,7 @@ dependencies = [ [[package]] name = "photocraft-color" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-cms", "serde", @@ -4321,7 +4321,7 @@ dependencies = [ [[package]] name = "photocraft-compose" -version = "0.8.1" +version = "0.8.2" dependencies = [ "criterion", "photocraft-color", @@ -4336,7 +4336,7 @@ dependencies = [ [[package]] name = "photocraft-doc" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4347,7 +4347,7 @@ dependencies = [ [[package]] name = "photocraft-engine" -version = "0.8.1" +version = "0.8.2" dependencies = [ "blake3", "exr", @@ -4381,7 +4381,7 @@ dependencies = [ [[package]] name = "photocraft-format" -version = "0.8.1" +version = "0.8.2" dependencies = [ "blake3", "crc32fast", @@ -4401,14 +4401,14 @@ dependencies = [ [[package]] name = "photocraft-geom" -version = "0.8.1" +version = "0.8.2" dependencies = [ "serde", ] [[package]] name = "photocraft-gpu" -version = "0.8.1" +version = "0.8.2" dependencies = [ "half", "log", @@ -4425,14 +4425,14 @@ dependencies = [ [[package]] name = "photocraft-heif" -version = "0.8.1" +version = "0.8.2" dependencies = [ "heic-rs", ] [[package]] name = "photocraft-io" -version = "0.8.1" +version = "0.8.2" dependencies = [ "blake3", "crc32fast", @@ -4461,14 +4461,14 @@ dependencies = [ [[package]] name = "photocraft-ops" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-doc", ] [[package]] name = "photocraft-paint" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4479,7 +4479,7 @@ dependencies = [ [[package]] name = "photocraft-plugins" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4494,7 +4494,7 @@ dependencies = [ [[package]] name = "photocraft-psd" -version = "0.8.1" +version = "0.8.2" dependencies = [ "flate2", "photocraft-psd", @@ -4505,7 +4505,7 @@ dependencies = [ [[package]] name = "photocraft-raster" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-geom", @@ -4515,7 +4515,7 @@ dependencies = [ [[package]] name = "photocraft-raw" -version = "0.8.1" +version = "0.8.2" dependencies = [ "flate2", "image", @@ -4530,7 +4530,7 @@ dependencies = [ [[package]] name = "photocraft-tablet" -version = "0.8.1" +version = "0.8.2" dependencies = [ "block2 0.6.2", "objc2 0.6.4", @@ -4542,7 +4542,7 @@ dependencies = [ [[package]] name = "photocraft-testkit" -version = "0.8.1" +version = "0.8.2" dependencies = [ "memory-stats", "photocraft-color", @@ -4553,7 +4553,7 @@ dependencies = [ [[package]] name = "photocraft-text" -version = "0.8.1" +version = "0.8.2" dependencies = [ "flate2", "parley", @@ -4567,7 +4567,7 @@ dependencies = [ [[package]] name = "photocraft-ui-egui" -version = "0.8.1" +version = "0.8.2" dependencies = [ "base64", "craft-ui", @@ -4607,7 +4607,7 @@ dependencies = [ [[package]] name = "photocraft-vector" -version = "0.8.1" +version = "0.8.2" dependencies = [ "photocraft-color", "photocraft-doc", @@ -4618,7 +4618,7 @@ dependencies = [ [[package]] name = "photocraft-web" -version = "0.8.1" +version = "0.8.2" dependencies = [ "eframe", "egui", @@ -7451,7 +7451,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "xtask" -version = "0.8.1" +version = "0.8.2" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f075c89d03..811ee7cbee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/photocraft", "apps/photocraft-cli", "xtask"] [workspace.package] -version = "0.8.1" +version = "0.8.2" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95"