mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Print: Functions › Bleed (corner crop marks on the trim line)
Photoshop's Print dialog has Functions › Bleed: crop marks print inside
the image by the bleed width, so trimming on them cuts through the
artwork and leaves no paper-white sliver. target-app-parity listed it as
missing ("No Separations/bleed").
- file.print / file.printOneCopy take "bleed" in inches (default 0),
report it back, and Print One Copy remembers it.
- Corner crop marks move onto the trim line, still starting 6 pt outside
the image. Centre and registration marks are unchanged (a symmetric
bleed doesn't move the midlines).
- One helper (corner_crop_segments) drives both the PDF writer and the
native Windows primitives; the dialog preview draws the marks there too.
- Negative, non-numeric or too-wide bleeds are BadParams, never a panic;
bleed without corner crop marks changes nothing and reports 0.
- With bleed absent or 0 the PDF is byte-identical (tested).
The Print dialog field is a follow-up: every tl! label needs all
translations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2b3d05636f
commit
6f2abe20fc
@@ -61,6 +61,27 @@ pub struct Marks {
|
||||
pub corner_crop: bool,
|
||||
pub center_crop: bool,
|
||||
pub registration: bool,
|
||||
/// Photoshop's Functions › Bleed, in points: corner crop marks sit this far inside the image
|
||||
/// edge, so the trim cuts through the image and leaves no paper-white sliver. 0 = on the edge.
|
||||
pub bleed: f64,
|
||||
}
|
||||
|
||||
/// Gap between the image edge and a crop mark, and the mark's length (pt).
|
||||
const MARK_GAP: f64 = 6.0;
|
||||
const MARK_LEN: f64 = 18.0;
|
||||
|
||||
/// Corner crop marks around `(x, y, w, h)` as eight `(from, to)` segments, a horizontal then a
|
||||
/// vertical one per corner. Each starts `MARK_GAP` outside the image (so it never prints on it)
|
||||
/// and lies on the trim line, `bleed` inside the edge. The inset is symmetric, so this holds for
|
||||
/// both bottom-up (PDF) and top-down (Windows) coordinates.
|
||||
fn corner_crop_segments(bleed: f64, (x, y, w, h): (f64, f64, f64, f64)) -> [((f64, f64), (f64, f64)); 8] {
|
||||
let mut out = [((0.0, 0.0), (0.0, 0.0)); 8];
|
||||
for (i, (cx, cy, dx, dy)) in [(x, y, -1.0, -1.0), (x + w, y, 1.0, -1.0), (x, y + h, -1.0, 1.0), (x + w, y + h, 1.0, 1.0)].into_iter().enumerate() {
|
||||
let (tx, ty) = (cx - dx * bleed, cy - dy * bleed);
|
||||
out[2 * i] = ((cx + dx * MARK_GAP, ty), (cx + dx * (MARK_GAP + MARK_LEN), ty));
|
||||
out[2 * i + 1] = ((tx, cy + dy * MARK_GAP), (tx, cy + dy * (MARK_GAP + MARK_LEN)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// One page: paper (pt), image box (pt, origin bottom-left), marks and texts.
|
||||
@@ -102,12 +123,11 @@ fn deflate(data: &[u8]) -> Vec<u8> {
|
||||
/// Vector marks around `(x, y, w, h)`.
|
||||
fn marks_ops(m: Marks, (x, y, w, h): (f64, f64, f64, f64)) -> String {
|
||||
let mut s = String::from("q 0 G 0.3 w\n");
|
||||
let (gap, len) = (6.0, 18.0);
|
||||
let (gap, len) = (MARK_GAP, MARK_LEN);
|
||||
let line = |s: &mut String, x0: f64, y0: f64, x1: f64, y1: f64| s.push_str(&format!("{x0:.2} {y0:.2} m {x1:.2} {y1:.2} l S\n"));
|
||||
if m.corner_crop {
|
||||
for (cx, cy, dx, dy) in [(x, y, -1.0, -1.0), (x + w, y, 1.0, -1.0), (x, y + h, -1.0, 1.0), (x + w, y + h, 1.0, 1.0)] {
|
||||
line(&mut s, cx + dx * gap, cy, cx + dx * (gap + len), cy);
|
||||
line(&mut s, cx, cy + dy * gap, cx, cy + dy * (gap + len));
|
||||
for ((x0, y0), (x1, y1)) in corner_crop_segments(m.bleed, (x, y, w, h)) {
|
||||
line(&mut s, x0, y0, x1, y1);
|
||||
}
|
||||
}
|
||||
if m.center_crop {
|
||||
@@ -303,10 +323,16 @@ pub fn layout(doc: &Document, p: &Value, cmd: &str) -> Result<PrintLayout> {
|
||||
let (pw, ph) = paper(p, cmd)?;
|
||||
let dpi = f64::from(doc.resolution_dpi.max(1.0));
|
||||
let (iw, ih) = (f64::from(doc.size.width) * 72.0 / dpi, f64::from(doc.size.height) * 72.0 / dpi);
|
||||
let marks = Marks {
|
||||
// Bleed in inches, like Photoshop's Position fields; the marks need it in points.
|
||||
let bleed = match p.get("bleed") {
|
||||
None | Some(Value::Null) => 0.0,
|
||||
Some(v) => v.as_f64().filter(|b| b.is_finite() && *b >= 0.0).ok_or_else(|| bad(cmd, "bleed is a width in inches, 0 or more"))? * 72.0,
|
||||
};
|
||||
let mut marks = Marks {
|
||||
corner_crop: p.get("cornerCropMarks").and_then(Value::as_bool).unwrap_or(false),
|
||||
center_crop: p.get("centerCropMarks").and_then(Value::as_bool).unwrap_or(false),
|
||||
registration: p.get("registrationMarks").and_then(Value::as_bool).unwrap_or(false),
|
||||
bleed,
|
||||
};
|
||||
let margin = if marks.corner_crop || marks.center_crop || marks.registration { 36.0 } else { 18.0 };
|
||||
let fit = p.get("scaleToFit").and_then(Value::as_bool).unwrap_or(false);
|
||||
@@ -316,6 +342,14 @@ pub fn layout(doc: &Document, p: &Value, cmd: &str) -> Result<PrintLayout> {
|
||||
p.get("scale").and_then(Value::as_f64).filter(|v| *v > 0.0).unwrap_or(100.0) / 100.0
|
||||
};
|
||||
let (w, h) = (iw * scale, ih * scale);
|
||||
// The trim box must keep some area, or the marks of opposite corners would cross.
|
||||
if 2.0 * marks.bleed >= w.min(h) {
|
||||
return Err(bad(cmd, format!("bleed {:.3} in leaves no image inside the trim ({:.2} × {:.2} in printed)", marks.bleed / 72.0, w / 72.0, h / 72.0)));
|
||||
}
|
||||
// Bleed only moves crop marks; without them it has nothing to change.
|
||||
if !marks.corner_crop {
|
||||
marks.bleed = 0.0;
|
||||
}
|
||||
let center = p.get("center").and_then(Value::as_bool).unwrap_or(true);
|
||||
let (x, y) = if center {
|
||||
((pw - w) / 2.0, (ph - h) / 2.0)
|
||||
@@ -398,7 +432,7 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
|
||||
require_printing_enabled()?;
|
||||
let printed = print_windows(&page, p, &doc.name, copies)?;
|
||||
return Ok(json!({
|
||||
"pdf": Value::Null, "paper": [pw, ph], "imageRect": [x, y, w, h], "emulsionDown": emulsion_down, "scale": scale * 100.0,
|
||||
"pdf": Value::Null, "paper": [pw, ph], "imageRect": [x, y, w, h], "emulsionDown": emulsion_down, "bleed": marks.bleed / 72.0, "scale": scale * 100.0,
|
||||
"copies": copies, "command": Value::Null, "sent": true, "spooler": printed, "color": color,
|
||||
}));
|
||||
}
|
||||
@@ -431,7 +465,7 @@ fn render_and_spool(doc: &Document, p: &Value, cmd: &str) -> Result<Value> {
|
||||
sent = true;
|
||||
}
|
||||
Ok(
|
||||
json!({"pdf": pdf_path, "bytes": pdf.len(), "paper": [pw, ph], "imageRect": [x, y, w, h], "emulsionDown": emulsion_down, "scale": scale * 100.0, "copies": copies, "command": command, "sent": sent, "spooler": spool, "color": color}),
|
||||
json!({"pdf": pdf_path, "bytes": pdf.len(), "paper": [pw, ph], "imageRect": [x, y, w, h], "emulsionDown": emulsion_down, "bleed": marks.bleed / 72.0, "scale": scale * 100.0, "copies": copies, "command": command, "sent": sent, "spooler": spool, "color": color}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -750,7 +784,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
CommandSpec { id: $id, label: $label, menu: $menu, shortcut: $sc, params: $params, enabled: $enabled, journal: true, run: $run }
|
||||
};
|
||||
}
|
||||
const PRINT_PARAMS: &str = r##"{"printerSettings":hex? (Windows driver settings),"printer":name? (default printer),"copies":1..999=1,"paper":"letter|legal|tabloid|a3|a4|a5|4x6|5x7"|[w,h] pt="letter","orientation":"portrait|landscape"="portrait","center":bool=true,"top":in?,"left":in?,"scale":%=100,"scaleToFit":bool=false,"colorHandling":"printerManages|photocraftManages|noColorManagement"="printerManages","printerProfile":profile? (photocraftManages),"intent":"perceptual|relative|saturation|absolute"="relative","bpc":bool=true,"cornerCropMarks":bool,"centerCropMarks":bool,"registrationMarks":bool,"description":bool,"labels":bool,"emulsionDown":bool=false (mirror the page left to right, for sublimation and other transfer printing),"output":pdf path? (print to PDF; then "send" defaults to false),"send":bool?,"dryRun":bool=false (render the PDF, report the backend command, don't spool; write a PDF only at an explicit output path)} → {pdf:path|null (null without explicit output), imageRect, command:diagnostic array|null, sent}"##;
|
||||
const PRINT_PARAMS: &str = r##"{"printerSettings":hex? (Windows driver settings),"printer":name? (default printer),"copies":1..999=1,"paper":"letter|legal|tabloid|a3|a4|a5|4x6|5x7"|[w,h] pt="letter","orientation":"portrait|landscape"="portrait","center":bool=true,"top":in?,"left":in?,"scale":%=100,"scaleToFit":bool=false,"colorHandling":"printerManages|photocraftManages|noColorManagement"="printerManages","printerProfile":profile? (photocraftManages),"intent":"perceptual|relative|saturation|absolute"="relative","bpc":bool=true,"cornerCropMarks":bool,"centerCropMarks":bool,"registrationMarks":bool,"bleed":in=0 (Functions › Bleed: corner crop marks this far inside the image edge, on the trim line),"description":bool,"labels":bool,"emulsionDown":bool=false (mirror the page left to right, for sublimation and other transfer printing),"output":pdf path? (print to PDF; then "send" defaults to false),"send":bool?,"dryRun":bool=false (render the PDF, report the backend command, don't spool; write a PDF only at an explicit output path)} → {pdf:path|null (null without explicit output), imageRect, command:diagnostic array|null, sent}"##;
|
||||
|
||||
vec![
|
||||
spec!("file.print", "Print…", &["File"], Some("Cmd+P"), PRINT_PARAMS, native_doc, print),
|
||||
|
||||
@@ -434,3 +434,74 @@ fn printers_lists_the_spooler_destinations_without_a_document() {
|
||||
None => assert!(list.iter().all(|p| p["default"] == false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// A dry-run print to `name`.pdf with `extra` params; returns the result and the PDF content stream.
|
||||
fn print_with(s: &mut Session, dir: &str, name: &str, extra: Value) -> (Value, Vec<u8>) {
|
||||
let mut p = json!({"output": format!("{dir}/{name}.pdf"), "dryRun": true, "paper": "a4", "center": false, "left": 1.0, "top": 1.0});
|
||||
for (k, v) in extra.as_object().unwrap() {
|
||||
p[k] = v.clone();
|
||||
}
|
||||
let r = s.execute("file.print", p).unwrap();
|
||||
let pdf = std::fs::read(r["pdf"].as_str().unwrap()).unwrap();
|
||||
(r, pdf)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_bleed_prints_exactly_as_before() {
|
||||
let mut s = session("rgb", 8);
|
||||
let dir = tmp("bleed-zero");
|
||||
let (plain, a) = print_with(&mut s, &dir, "plain", json!({"cornerCropMarks": true}));
|
||||
let (zero, b) = print_with(&mut s, &dir, "zero", json!({"cornerCropMarks": true, "bleed": 0.0}));
|
||||
assert_eq!(a, b, "bleed 0 must not change a single byte");
|
||||
assert_eq!(plain["bleed"], 0.0);
|
||||
assert_eq!(zero["bleed"], 0.0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bleed_puts_corner_crop_marks_on_the_trim_line_outside_the_image() {
|
||||
let mut s = session("rgb", 8);
|
||||
let dir = tmp("bleed");
|
||||
let (r, pdf) = print_with(&mut s, &dir, "bleed", json!({"cornerCropMarks": true, "centerCropMarks": true, "bleed": 0.125}));
|
||||
assert_eq!(r["bleed"], 0.125);
|
||||
let rect: Vec<f64> = r["imageRect"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||
let (x, y, w, h) = (rect[0], rect[1], rect[2], rect[3]);
|
||||
let c = pdf_content(&pdf);
|
||||
// Bottom-left corner (PDF is bottom-up): the horizontal mark runs left from 6 pt outside the
|
||||
// image, 9 pt (0.125 in) above the image's bottom edge; the vertical one 9 pt right of its left edge.
|
||||
assert!(c.contains(&format!("{:.2} {:.2} m {:.2} {:.2} l S", x - 6.0, y + 9.0, x - 24.0, y + 9.0)), "{c}");
|
||||
assert!(c.contains(&format!("{:.2} {:.2} m {:.2} {:.2} l S", x + 9.0, y - 6.0, x + 9.0, y - 24.0)), "{c}");
|
||||
// Top-right corner, mirrored.
|
||||
assert!(c.contains(&format!("{:.2} {:.2} m {:.2} {:.2} l S", x + w + 6.0, y + h - 9.0, x + w + 24.0, y + h - 9.0)), "{c}");
|
||||
assert!(c.contains(&format!("{:.2} {:.2} m {:.2} {:.2} l S", x + w - 9.0, y + h + 6.0, x + w - 9.0, y + h + 24.0)), "{c}");
|
||||
// Centre crop marks are on the midlines, which a symmetric bleed does not move.
|
||||
assert!(c.contains(&format!("{:.2} {:.2} m {:.2} {:.2} l S", x + w / 2.0, y - 6.0, x + w / 2.0, y - 24.0)), "{c}");
|
||||
// Print One Copy repeats the last print, bleed included.
|
||||
let again = s.execute("file.printOneCopy", json!({"output": format!("{dir}/again.pdf"), "dryRun": true})).unwrap();
|
||||
assert_eq!(again["bleed"], 0.125);
|
||||
// The image box itself is unchanged: bleed moves marks, not pixels.
|
||||
let (plain, _) = print_with(&mut s, &dir, "plain", json!({"cornerCropMarks": true}));
|
||||
assert_eq!(plain["imageRect"], r["imageRect"]);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bleed_without_corner_crop_marks_changes_nothing() {
|
||||
let mut s = session("rgb", 8);
|
||||
let dir = tmp("bleed-nomarks");
|
||||
let (_, a) = print_with(&mut s, &dir, "plain", json!({"registrationMarks": true}));
|
||||
let (r, b) = print_with(&mut s, &dir, "bleed", json!({"registrationMarks": true, "bleed": 0.1}));
|
||||
assert_eq!(a, b);
|
||||
assert_eq!(r["bleed"], 0.0);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_bleed_is_an_error_not_a_panic() {
|
||||
let mut s = session("rgb", 8);
|
||||
// The test document prints 300 × 150 pt, so 1.05 in (75.6 pt) leaves no trim box.
|
||||
for bleed in [json!(-0.1), json!("0.1"), json!(true), json!([0.1]), json!(1.05), json!(1e308)] {
|
||||
let r = s.execute("file.print", json!({"dryRun": true, "send": false, "output": "", "cornerCropMarks": true, "bleed": bleed}));
|
||||
assert!(matches!(r, Err(EngineError::BadParams { .. })), "bleed {bleed}: {r:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! App-owned print-mark composition in points from the sheet's top-left.
|
||||
use super::Marks;
|
||||
use super::{MARK_GAP, MARK_LEN, Marks, corner_crop_segments};
|
||||
use craft_print::rgb::{Primitive, Shape};
|
||||
|
||||
pub(super) fn primitives(marks: Marks, (x, y, w, h): (f64, f64, f64, f64)) -> Vec<Primitive> {
|
||||
@@ -7,11 +7,10 @@ pub(super) fn primitives(marks: Marks, (x, y, w, h): (f64, f64, f64, f64)) -> Ve
|
||||
let line = |out: &mut Vec<Primitive>, x0, y0, x1, y1| {
|
||||
out.push(Primitive { shape: Shape::Line { from: (x0, y0), to: (x1, y1) }, width: 0.3, color: [0; 3] });
|
||||
};
|
||||
let (gap, len) = (6.0, 18.0);
|
||||
let (gap, len) = (MARK_GAP, MARK_LEN);
|
||||
if marks.corner_crop {
|
||||
for (cx, cy, dx, dy) in [(x, y, -1.0, -1.0), (x + w, y, 1.0, -1.0), (x, y + h, -1.0, 1.0), (x + w, y + h, 1.0, 1.0)] {
|
||||
line(&mut out, cx + dx * gap, cy, cx + dx * (gap + len), cy);
|
||||
line(&mut out, cx, cy + dy * gap, cx, cy + dy * (gap + len));
|
||||
for ((x0, y0), (x1, y1)) in corner_crop_segments(marks.bleed, (x, y, w, h)) {
|
||||
line(&mut out, x0, y0, x1, y1);
|
||||
}
|
||||
}
|
||||
if marks.center_crop {
|
||||
@@ -37,11 +36,24 @@ mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn marks_surround_the_final_image_box_in_physical_points() {
|
||||
let out = primitives(Marks { corner_crop: true, center_crop: true, registration: true }, (100.0, 200.0, 144.0, 72.0));
|
||||
let out = primitives(Marks { corner_crop: true, center_crop: true, registration: true, bleed: 0.0 }, (100.0, 200.0, 144.0, 72.0));
|
||||
assert_eq!(out.len(), 24);
|
||||
assert_eq!(out[0].shape, Shape::Line { from: (94.0, 200.0), to: (76.0, 200.0) });
|
||||
assert_eq!(out[12].shape, Shape::Ellipse { bounds: (167.0, 180.0, 10.0, 10.0) });
|
||||
assert!(out.iter().all(|p| p.width == 0.3 && p.color == [0; 3]));
|
||||
assert!(primitives(Marks::default(), (0.0, 0.0, 10.0, 10.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bleed_moves_corner_crop_marks_onto_the_trim_line_in_top_down_coordinates() {
|
||||
let rect = (100.0, 200.0, 144.0, 72.0);
|
||||
let out = primitives(Marks { corner_crop: true, bleed: 9.0, ..Marks::default() }, rect);
|
||||
assert_eq!(out.len(), 8);
|
||||
// Top-left corner: still outside the image, but on the trim line 9 pt in (y down here).
|
||||
assert_eq!(out[0].shape, Shape::Line { from: (94.0, 209.0), to: (76.0, 209.0) });
|
||||
assert_eq!(out[1].shape, Shape::Line { from: (109.0, 194.0), to: (109.0, 176.0) });
|
||||
// Bottom-right corner, mirrored.
|
||||
assert_eq!(out[6].shape, Shape::Line { from: (250.0, 263.0), to: (268.0, 263.0) });
|
||||
assert_eq!(out[7].shape, Shape::Line { from: (235.0, 278.0), to: (235.0, 296.0) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -921,11 +921,13 @@ fn print_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map<String, Va
|
||||
clip.image(tex.id(), img, uv, egui::Color32::WHITE);
|
||||
let mark = egui::Stroke::new(1.0, egui::Color32::BLACK);
|
||||
if l.marks.corner_crop {
|
||||
// Bleed puts the marks on the trim line, inside the image edge.
|
||||
let b = l.marks.bleed as f32 * k;
|
||||
for (c, dx, dy) in
|
||||
[(img.left_top(), -1.0, -1.0), (img.right_top(), 1.0, -1.0), (img.left_bottom(), -1.0, 1.0), (img.right_bottom(), 1.0, 1.0)]
|
||||
{
|
||||
clip.line_segment([c + egui::vec2(dx * 3.0, 0.0), c + egui::vec2(dx * 12.0, 0.0)], mark);
|
||||
clip.line_segment([c + egui::vec2(0.0, dy * 3.0), c + egui::vec2(0.0, dy * 12.0)], mark);
|
||||
clip.line_segment([c + egui::vec2(dx * 3.0, -dy * b), c + egui::vec2(dx * 12.0, -dy * b)], mark);
|
||||
clip.line_segment([c + egui::vec2(-dx * b, dy * 3.0), c + egui::vec2(-dx * b, dy * 12.0)], mark);
|
||||
}
|
||||
}
|
||||
if l.marks.registration {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# PhotoCraft vs Adobe Photoshop: target-app parity
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (on-canvas corner radius widgets, TOOL-214-12; `.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged) · **Target:** Adobe Photoshop 2026
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-15 · **Change:** minor (Print › Functions › Bleed through `file.print`; on-canvas corner radius widgets, TOOL-214-12; `.atn` import maps 112 events; Free Transform H/V skew fields; stale rows corrected against `main` and the scorecard, estimates unchanged; Photoshop Actions `.atn` load and save; 2026-10-10: Tilt-Shift measurements added; earlier the same day: major, full re-measure against Photoshop 2026 27.11.0; merged `parity-estimate.md` and the roadmap's honest assessment; stale missing-tool rows corrected: Color Replacement and Red Eye are live toolbar tools, Frame landed; percentages unchanged) · **Target:** Adobe Photoshop 2026
|
||||
|
||||
The authoritative answer to "how close is PhotoCraft to Photoshop, and how much work is left". The
|
||||
one-page summary is [`ROADMAP.md`](../ROADMAP.md); the ranked work list is [`gaps.md`](gaps.md);
|
||||
@@ -286,7 +286,7 @@ just presence.
|
||||
| Transform, warp, puppet, perspective, liquify | ~88% | No Face-Aware Liquify (needs a landmark model) |
|
||||
| Automation: CLI, MCP, headless server, control channel | ~95% | Ahead of Photoshop here (see below) |
|
||||
| Image modes (RGB/Gray/CMYK/Lab/Indexed/Duotone/Multichannel) | ~85% | Multichannel display-only; no ink-plate model |
|
||||
| Print / export / Save for Web / slices / Generator | ~85% | No Separations/bleed; droplets basic |
|
||||
| Print / export / Save for Web / slices / Generator | ~85% | No Separations; Bleed only through `file.print` (no Print dialog field yet); droplets basic |
|
||||
| Computational photo (Photomerge, HDR, Lens, Camera Raw, Wide Angle) | ~70% | Classical implementations; generic lens profiles only |
|
||||
| Data-driven graphics (Variables, Data Sets) | ~0% | Not started (5 menu items) |
|
||||
| Video / Timeline / animation | ~0% | Not started (13 menu items) — a whole subsystem |
|
||||
@@ -565,6 +565,7 @@ See [method, raw measurements and reproduction](five-algorithm-performance.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Print: Functions › Bleed (`file.print` / `file.printOneCopy` `bleed`, corner crop marks on the trim line, PDF and Windows paths); the Print dialog field is still missing; percentages unchanged |
|
||||
| 2026-10-13 | minor | Vector shapes: on-canvas corner radius widgets on live rectangles (TOOL-214-12); estimates unchanged |
|
||||
| 2026-10-13 | minor | Stale "missing tool" rows corrected against main: Color Replacement (`paint.colorReplacement`), Red Eye (`paint.redEye`) and Add/Delete Anchor Point (the Pen flyout) are live toolbar tools and Frame landed, so Art History Brush is the only tool still missing; percentages unchanged |
|
||||
| 2026-10-12 | minor | Free Transform options bar H/V skew fields; percentages unchanged |
|
||||
|
||||
Reference in New Issue
Block a user