O: Frame Fitting Options — auto-fit on resize, align from, crop (+ dialog, IDML)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 00:14:47 -04:00
1 parent 4f18835eb3
commit 2f54f0b53d
13 files changed
+340 -15

No files matched your search

+1
View File
@@ -58,6 +58,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Hyperlinks and Bookmarks panels (Window ▸ Interactive): new from the selection, edit (`hyperlink.edit`), go to source (`hyperlink.goToSource`), rename bookmarks (`bookmark.rename`), go to page, delete.
- Nested line styles (`nestedLineStyles` paragraph attribute, Paragraph Style Options › Drop Caps and Nested Styles, IDML AllNestedLineStyles): the first lines restyle until the line breaks settle.
- User dictionary hyphenation exceptions (`hyphenation.addException/removeException/list`, Edit ▸ Spelling ▸ User Dictionary): `ex~am~ple` breaks only at `~`, a plain word never hyphenates.
- Frame Fitting Options (`object.fittingOptions`, Object ▸ Fitting ▸ Frame Fitting Options…): Auto-Fit refits on resize, fitting, Align From reference point, crop amounts; IDML FrameFittingOption.
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
+2
View File
@@ -244,6 +244,8 @@ pub fn synthetic(spec: &Spec) -> Document {
size: (nw, nh),
xf: Affine::translate((r.x0 + (r.width() - nw * s) / 2.0, r.y0 + (r.height() - nh * s) / 2.0)) * Affine::scale(s),
auto_fit: designcraft_doc::Fitting::FillProportionally,
fit_align: 4,
crop: [0.0; 4],
});
if k == 0 && pi == 0 {
it.effects.drop_shadow.on = true;
+45
View File
@@ -395,6 +395,51 @@ pub struct Graphic {
pub xf: Affine,
#[serde(default)]
pub auto_fit: Fitting,
/// Frame Fitting Options › Align From: the reference point (0–8, row-major; 4 = centre).
#[serde(default = "center_ref")]
pub fit_align: u8,
/// Frame Fitting Options › Crop Amount: top, left, bottom, right (negative adds space).
#[serde(default)]
pub crop: [f64; 4],
}
fn center_ref() -> u8 {
4
}
impl Graphic {
/// The graphic transform that fits it into `frame` (the frame's inner rect) with `mode`,
/// honouring the crop amounts and the reference point.
pub fn fitted(&self, frame: Rect, mode: Fitting) -> Option<Affine> {
let (nw, nh) = self.size;
if nw <= 0.0 || nh <= 0.0 {
return None;
}
let [t, l, b, r] = self.crop;
let area = Rect::new(frame.x0 - l, frame.y0 - t, frame.x1 + r, frame.y1 + b);
if area.width() <= 0.0 || area.height() <= 0.0 {
return None;
}
let (fx, fy) = ([0.0, 0.5, 1.0][(self.fit_align % 3) as usize], [0.0, 0.5, 1.0][(self.fit_align / 3).min(2) as usize]);
let place = |w: f64, h: f64| (area.x0 + (area.width() - w) * fx, area.y0 + (area.height() - h) * fy);
Some(match mode {
Fitting::None => return None,
Fitting::FillProportionally | Fitting::FitProportionally => {
let k = if mode == Fitting::FillProportionally {
(area.width() / nw).max(area.height() / nh)
} else {
(area.width() / nw).min(area.height() / nh)
};
Affine::translate(place(nw * k, nh * k)) * Affine::scale(k)
}
Fitting::FitContentToFrame => Affine::translate((area.x0, area.y0)) * Affine::scale_non_uniform(area.width() / nw, area.height() / nh),
Fitting::CenterContent => {
let cur = self.xf.transform_rect_bbox(Rect::new(0.0, 0.0, nw, nh));
let (x, y) = place(cur.width(), cur.height());
Affine::translate((x - cur.x0, y - cur.y0)) * self.xf
}
})
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+8
View File
@@ -253,6 +253,8 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
size: (nw, nh),
xf: Affine::translate((r.x0 + (r.width() - gw) / 2.0, r.y0 + (r.height() - gh) / 2.0)) * Affine::scale(k),
auto_fit: designcraft_doc::Fitting::FillProportionally,
fit_align: 4,
crop: [0.0; 4],
});
fid
} else {
@@ -267,6 +269,8 @@ fn file_place(s: &mut Session, p: &Value) -> Result<Value> {
size: (nw, nh),
xf: Affine::translate((x, y)) * Affine::scale(w / nw),
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
});
d.insert_item(spread, it, None)?;
id
@@ -361,6 +365,8 @@ fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
size: (nw, nh),
xf: Affine::translate((r.x0 + (r.width() - nw * k) / 2.0, r.y0 + (r.height() - nh * k) / 2.0)) * Affine::scale(k),
auto_fit: designcraft_doc::Fitting::FillProportionally,
fit_align: 4,
crop: [0.0; 4],
});
fid
}
@@ -381,6 +387,8 @@ fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
size: (nw, nh),
xf: Affine::translate((frame_r.x0, frame_r.y0)) * Affine::scale(k),
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
});
d.insert_item(sr, it, None)?;
id
+122 -10
View File
@@ -376,6 +376,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_selection,
fit
),
cmd!(
"object.fittingOptions",
"Frame Fitting Options…",
["Object", "Fitting"],
None,
"{autoFit?: bool, fitting?: none|fillProportionally|fitProportionally|fitContentToFrame|centerContent, align?: 0..8 (reference point, 4 = centre), crop?: n | [t, l, b, r] (negative adds space), ids?}",
has_selection,
fitting_options
),
cmd!("object.rename", "Rename", [], None, "{id, name}", has_doc, |s, p| {
let name = str_param(p, "name").unwrap_or("").to_string();
set_flag(s, p, move |i| i.name = name.clone(), false)
@@ -743,9 +752,16 @@ fn transform_resize(s: &mut Session, p: &Value) -> Result<Value> {
} else if matches!(it.content, Content::Group { .. }) {
it.xf = m * it.xf;
} else {
// Resize the frame's path (in inner space); content keeps its size (InDesign's default).
// Resize the frame's path (in inner space); content keeps its size (InDesign's
// default) unless the frame auto-fits it.
let inner = it.xf.inverse() * m * it.xf;
bake(it, inner);
let r = it.inner_bounds();
if let Content::Graphic(g) = &mut it.content
&& let Some(xf) = g.fitted(r, g.auto_fit)
{
g.xf = xf;
}
}
}
Ok(json!({"resized": ids.len()}))
@@ -1206,6 +1222,65 @@ fn text_frame_options(s: &mut Session, p: &Value) -> Result<Value> {
})
}
fn fitting_mode(s: &str) -> Option<designcraft_doc::Fitting> {
use designcraft_doc::Fitting as F;
Some(match s {
"none" => F::None,
"fillProportionally" => F::FillProportionally,
"fitProportionally" => F::FitProportionally,
"fitContentToFrame" => F::FitContentToFrame,
"centerContent" => F::CenterContent,
_ => return None,
})
}
/// Frame Fitting Options: auto-fit, fitting, align from, crop amounts.
fn fitting_options(s: &mut Session, p: &Value) -> Result<Value> {
const ID: &str = "object.fittingOptions";
let ids = targets(s, p)?;
let fitting = match str_param(p, "fitting") {
Some(f) => Some(fitting_mode(f).ok_or_else(|| bad(ID, format!("unknown fitting `{f}`")))?),
None => None,
};
let auto = p.get("autoFit").and_then(Value::as_bool);
let align = p.get("align").and_then(Value::as_u64).map(|v| v.min(8) as u8);
let crop = match p.get("crop") {
Some(Value::Array(a)) if a.len() == 4 => Some([0, 1, 2, 3].map(|i| a[i].as_f64().unwrap_or(0.0))),
Some(Value::Number(n)) => Some([n.as_f64().unwrap_or(0.0); 4]),
Some(v) => return Err(bad(ID, format!("bad `crop`: {v} (n or [t, l, b, r])"))),
None => None,
};
s.edit(|d, _| {
let mut n = 0;
for id in &ids {
let Some(it) = d.item_mut(*id) else { continue };
let r = it.inner_bounds();
let Content::Graphic(g) = &mut it.content else { continue };
if let Some(a) = align {
g.fit_align = a;
}
if let Some(c) = crop {
g.crop = c;
}
let mode = fitting.unwrap_or(g.auto_fit);
if let Some(a) = auto {
g.auto_fit = if a {
if mode == designcraft_doc::Fitting::None { designcraft_doc::Fitting::FillProportionally } else { mode }
} else {
designcraft_doc::Fitting::None
};
} else if fitting.is_some() && g.auto_fit != designcraft_doc::Fitting::None {
g.auto_fit = mode;
}
if let Some(xf) = g.fitted(r, mode) {
g.xf = xf;
}
n += 1;
}
Ok(json!({"changed": n}))
})
}
fn fit(s: &mut Session, p: &Value) -> Result<Value> {
let ids = targets(s, p)?;
let mode = str_param(p, "mode").unwrap_or("fitProportionally").to_string();
@@ -1216,15 +1291,15 @@ fn fit(s: &mut Session, p: &Value) -> Result<Value> {
let Content::Graphic(g) = &mut it.content else { continue };
let (nw, nh) = g.size;
let cur = g.xf.transform_rect_bbox(Rect::new(0.0, 0.0, nw, nh));
match mode.as_str() {
"fillProportionally" | "fitProportionally" => {
let k = if mode == "fillProportionally" { (r.width() / nw).max(r.height() / nh) } else { (r.width() / nw).min(r.height() / nh) };
g.xf = Affine::translate((r.x0 + (r.width() - nw * k) / 2.0, r.y0 + (r.height() - nh * k) / 2.0)) * Affine::scale(k);
}
"fitContentToFrame" => g.xf = Affine::translate((r.x0, r.y0)) * Affine::scale_non_uniform(r.width() / nw, r.height() / nh),
"centerContent" => g.xf = Affine::translate(r.center() - cur.center()) * g.xf,
"fitFrameToContent" => it.path = shapes::rectangle(cur),
_ => return Err(bad("object.fit", format!("unknown mode `{mode}`"))),
if mode == "fitFrameToContent" {
it.path = shapes::rectangle(cur);
continue;
}
let m = fitting_mode(&mode)
.filter(|m| *m != designcraft_doc::Fitting::None)
.ok_or_else(|| bad("object.fit", format!("unknown mode `{mode}`")))?;
if let Some(xf) = g.fitted(r, m) {
g.xf = xf;
}
}
ok()
@@ -1709,3 +1784,40 @@ mod attributes_tests {
assert!(it.stroke.gap_overprint);
}
}
#[cfg(test)]
mod fitting_tests {
use super::*;
#[test]
fn frame_fitting_options_align_crop_and_auto_fit() {
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
// A 40×20 pt image placed into a 200×200 frame.
let png = designcraft_render::Rendered { width: 40, height: 20, pixels: vec![200; 40 * 20 * 4] }.to_png();
let f = s.execute("frame.create", &json!({"rect": [100, 100, 300, 300]})).unwrap()["id"].as_u64().unwrap();
s.execute("file.place", &json!({"base64": super::super::file::base64_encode(&png), "name": "a.png", "frame": f})).unwrap();
let content = |s: &Session| {
let it = s.doc().unwrap().doc.item(ItemId(f)).unwrap().clone();
let Content::Graphic(g) = &it.content else { panic!("graphic") };
it.xf.transform_rect_bbox(g.xf.transform_rect_bbox(Rect::new(0.0, 0.0, g.size.0, g.size.1)))
};
// Fit proportionally from the top-left: 200 wide, 100 high, against the top.
s.execute("object.fittingOptions", &json!({"ids": [f], "fitting": "fitProportionally", "align": 0})).unwrap();
let c = content(&s);
assert!((c.x0 - 100.0).abs() < 1e-6 && (c.y0 - 100.0).abs() < 1e-6 && (c.width() - 200.0).abs() < 1e-6, "{c:?}");
// Crop 10 pt on every side: the content spreads past the frame.
s.execute("object.fittingOptions", &json!({"ids": [f], "crop": 10, "align": 4})).unwrap();
let c = content(&s);
assert!((c.width() - 220.0).abs() < 1e-6, "{c:?}");
// Auto-fit: resizing the frame refits the content.
s.execute("object.fittingOptions", &json!({"ids": [f], "autoFit": true, "fitting": "fillProportionally", "crop": 0})).unwrap();
s.execute("transform.resize", &json!({"from": [100, 100, 300, 300], "to": [100, 100, 500, 300], "ids": [f]})).unwrap();
let c = content(&s);
assert!((c.width() - 400.0).abs() < 1e-6, "fills the wider frame: {c:?}");
// IDML keeps the options.
let back = designcraft_idml::import_idml(&designcraft_idml::export_idml(&s.doc().unwrap().doc)).unwrap();
let g = back.spreads.iter().flat_map(|sp| sp.items.iter()).find_map(|i| i.graphic().cloned()).unwrap();
assert_eq!(g.auto_fit, designcraft_doc::Fitting::FillProportionally);
}
}
+2
View File
@@ -83,6 +83,8 @@ fn graphic_frame(d: &mut Document, sr: SpreadRef, r: Rect, asset: AssetId, px: (
size: (nw, nh),
xf: Affine::translate((r.x0 + (r.width() - nw * k) / 2.0, r.y0 + (r.height() - nh * k) / 2.0)) * Affine::scale(k),
auto_fit: designcraft_doc::Fitting::FillProportionally,
fit_align: 4,
crop: [0.0; 4],
});
it.object_style = designcraft_doc::BASIC_GRAPHICS_FRAME.into();
d.insert_item(sr, it, None).expect("spread");
+12
View File
@@ -1446,6 +1446,18 @@ impl<'a> Ex<'a> {
}
}
Content::Graphic(g) => {
if g.auto_fit != designcraft_doc::Fitting::None || g.fit_align != 4 || g.crop != [0.0; 4] {
el.push(
El::new("FrameFittingOption")
.attr("AutoFit", bool_s(g.auto_fit != designcraft_doc::Fitting::None))
.attr("TopCrop", num(g.crop[0]))
.attr("LeftCrop", num(g.crop[1]))
.attr("BottomCrop", num(g.crop[2]))
.attr("RightCrop", num(g.crop[3]))
.attr("FittingOnEmptyFrame", names::fitting_out(g.auto_fit))
.attr("FittingAlignment", names::anchor_out(g.fit_align)),
);
}
if let Some(asset) = d.assets.get(&g.asset) {
let e = self.image_el(asset, g);
el.push(e);
+18 -1
View File
@@ -1887,6 +1887,16 @@ impl<'r> Importer<'r> {
_ => {
if let Some(g) = e.elements().find(|c| matches!(c.local(), "Image" | "PDF" | "EPS" | "ImportedPage" | "WMF" | "PICT" | "SVG")) {
it.content = self.graphic(g);
if let (Some(ff), Content::Graphic(gr)) = (e.elements().find(|c| c.local() == "FrameFittingOption"), &mut it.content) {
gr.crop = [ff.num("TopCrop"), ff.num("LeftCrop"), ff.num("BottomCrop"), ff.num("RightCrop")].map(|v| v.unwrap_or(0.0));
gr.fit_align = ff.get("FittingAlignment").map_or(4, names::anchor_in);
if ff.get("AutoFit") == Some("true") {
gr.auto_fit = ff.get("FittingOnEmptyFrame").map_or(designcraft_doc::Fitting::FillProportionally, names::fitting_in);
if gr.auto_fit == designcraft_doc::Fitting::None {
gr.auto_fit = designcraft_doc::Fitting::FillProportionally;
}
}
}
} else {
// Page items pasted into this frame (Paste Into).
let kids: Vec<Arc<Item>> = e
@@ -1939,7 +1949,14 @@ impl<'r> Importer<'r> {
let id = AssetId(self.alloc());
let link_path = uri.filter(|p| p.contains('/') || p.contains('\\'));
self.assets.insert(id, Arc::new(Asset { page: 0, id, name, mime, link: link_path, data: Arc::new(data), pixels }));
Content::Graphic(designcraft_doc::Graphic { asset: id, size, xf: gxf * Affine::translate((l, t)), auto_fit: Default::default() })
Content::Graphic(designcraft_doc::Graphic {
asset: id,
size,
xf: gxf * Affine::translate((l, t)),
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
})
}
fn link_threads(&mut self) {
+42
View File
@@ -9,6 +9,48 @@ use designcraft_geom::Unit;
use designcraft_geom::corners::CornerShape;
/// `%` → `%25`, `:` → `%3a` (IDML Self-id escaping).
const ANCHORS: [&str; 9] = [
"TopLeftAnchor",
"TopCenterAnchor",
"TopRightAnchor",
"LeftCenterAnchor",
"CenterAnchor",
"RightCenterAnchor",
"BottomLeftAnchor",
"BottomCenterAnchor",
"BottomRightAnchor",
];
pub fn anchor_out(i: u8) -> &'static str {
ANCHORS[(i as usize).min(8)]
}
pub fn anchor_in(s: &str) -> u8 {
ANCHORS.iter().position(|a| *a == s).unwrap_or(4) as u8
}
pub fn fitting_out(f: designcraft_doc::Fitting) -> &'static str {
use designcraft_doc::Fitting as F;
match f {
F::None => "None",
F::FillProportionally => "FillProportionally",
F::FitProportionally => "Proportionally",
F::FitContentToFrame => "ContentToFrame",
F::CenterContent => "CenterContent",
}
}
pub fn fitting_in(s: &str) -> designcraft_doc::Fitting {
use designcraft_doc::Fitting as F;
match s {
"FillProportionally" => F::FillProportionally,
"Proportionally" => F::FitProportionally,
"ContentToFrame" => F::FitContentToFrame,
"CenterContent" => F::CenterContent,
_ => F::None,
}
}
pub fn escape_id(s: &str) -> String {
s.replace('%', "%25").replace(':', "%3a")
}
+2
View File
@@ -1050,6 +1050,8 @@ mod tests {
size: (100.0, 100.0),
xf: Affine::translate((100.0, 100.0)),
auto_fit: Default::default(),
fit_align: 4,
crop: [0.0; 4],
});
d.insert_item(SpreadRef::Doc(0), it, None).unwrap();
let cache = Cache::new();
+67
View File
@@ -641,6 +641,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"findFont" => "Find/Replace Font",
"polygonSettings" => "Polygon Settings",
"userDictionary" => "User Dictionary",
"fittingOptions" => "Frame Fitting Options",
"qrCode" => "Generate QR Code",
"preferences" => "Preferences",
"print" => "Print",
@@ -793,6 +794,64 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
}
"keyboardShortcuts" => keyboard_shortcuts(app, ui, &mut d),
"userDictionary" => user_dictionary(app, ui, &mut d),
"fittingOptions" => {
check(ui, &mut d, "autoFit", "Auto-Fit");
ui.add_space(4.0);
ui.label(egui::RichText::new("Content Fitting").font(semibold(12.0)));
let cur = d.s("fitting");
let label = |v: &str| match v {
"fillProportionally" => "Fill Frame Proportionally",
"fitProportionally" => "Fit Content Proportionally",
"fitContentToFrame" => "Fit Content to Frame",
"centerContent" => "Center Content",
_ => "None",
};
ui.horizontal(|ui| {
ui.label("Fitting:");
egui::ComboBox::from_id_salt("ff_fit").selected_text(label(&cur)).width(200.0).show_ui(ui, |ui| {
for v in ["none", "fillProportionally", "fitProportionally", "fitContentToFrame", "centerContent"] {
if ui.selectable_label(cur == v, label(v)).clicked() {
d.fields.insert("fitting".into(), json!(v));
}
}
});
});
ui.horizontal(|ui| {
ui.label("Align From:");
let a = d.n("align").unwrap_or(4.0) as u8;
egui::Grid::new("ff_align").spacing([2.0, 2.0]).show(ui, |ui| {
for row in 0..3u8 {
for col in 0..3u8 {
let i = row * 3 + col;
let (r, resp) = ui.allocate_exact_size(egui::vec2(14.0, 14.0), egui::Sense::click());
let c = ui.visuals().text_color();
if a == i {
ui.painter().rect_filled(r.shrink(2.0), 1.0, c);
} else {
ui.painter().rect_stroke(r.shrink(3.0), 1.0, egui::Stroke::new(1.0, c), egui::StrokeKind::Inside);
}
if resp.clicked() {
d.fields.insert("align".into(), json!(i));
}
}
ui.end_row();
}
});
});
ui.label(egui::RichText::new("Crop Amount").font(semibold(12.0)));
egui::Grid::new("ff_crop").num_columns(4).spacing([8.0, 6.0]).show(ui, |ui| {
for (k, l) in [("cropTop", "Top:"), ("cropLeft", "Left:")] {
ui.label(l);
text_field(ui, &mut d, k, 70.0);
}
ui.end_row();
for (k, l) in [("cropBottom", "Bottom:"), ("cropRight", "Right:")] {
ui.label(l);
text_field(ui, &mut d, k, 70.0);
}
ui.end_row();
});
}
"qrCode" => {
let cur = d.s("type");
ui.horizontal(|ui| {
@@ -1076,6 +1135,14 @@ pub fn confirm(app: &mut DesignApp) -> Result<Value, String> {
doc["overprintBlack"] = json!(d.b("overprintBlack"));
app.run("document.preferences", doc)
}
"fittingOptions" => {
let m = |k: &str| d.fields.get(k).and_then(Value::as_str).and_then(|s| parse_measure(s, Unit::Points).ok()).unwrap_or(0.0);
app.run(
"object.fittingOptions",
json!({"autoFit": d.b("autoFit"), "fitting": d.s("fitting"), "align": d.n("align").unwrap_or(4.0) as u64,
"crop": [m("cropTop"), m("cropLeft"), m("cropBottom"), m("cropRight")]}),
)
}
"qrCode" => {
let mut p = Value::Object(d.fields.clone());
// Nothing selected: a 2-inch code at the top left of the page in view.
+15
View File
@@ -22,6 +22,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
("app.printBooklet", "Print Booklet…", None, "{path?} — saddle-stitched printer spreads as PDF"),
("app.qrCode", "Generate QR Code…", None, "{} — the QR Code dialog (object.qrCode does the work)"),
("app.userDictionary", "User Dictionary…", None, "{} — hyphenation exceptions (hyphenation.* commands)"),
("app.fittingOptions", "Frame Fitting Options…", None, "{} — the dialog for object.fittingOptions"),
("app.exportText", "Export Text…", None, "{path?} — the story being edited, as Text Only (.txt) or Rich Text Format (.rtf)"),
("app.exportPdf", "Export PDF…", None, "{path?, …file.exportPdf options} — asks for a path when none is given"),
("app.palette", "Quick Apply…", Some("Cmd+Return"), "{} — search styles and commands"),
@@ -390,6 +391,8 @@ pub const MENUS: &[(&str, &[&str])] = &[
"cmd:object.fit|Fit Frame to Content|{\"mode\":\"fitFrameToContent\"}",
"cmd:object.fit|Fit Content to Frame|{\"mode\":\"fitContentToFrame\"}",
"cmd:object.fit|Center Content|{\"mode\":\"centerContent\"}",
"-",
"ui:app.fittingOptions",
"<",
">Content",
"cmd:object.content|Graphic|{\"type\":\"graphic\"}",
@@ -552,6 +555,18 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
"app.exportEpub" => export_bytes(app, p, "epub", "file.exportEpub"),
"app.exportHtml" => export_bytes(app, p, "html", "file.exportHtml"),
"app.printBooklet" => export_bytes(app, p, "pdf", "file.printBooklet"),
"app.fittingOptions" => {
let g = app.session.active().and_then(|d| d.selection.items.first().and_then(|id| d.doc.item(*id)).and_then(|it| it.graphic().cloned()));
let Some(g) = g else { return Some(Err("select a frame with a placed graphic".into())) };
let fitting = serde_json::to_value(g.auto_fit).unwrap_or(json!("none"));
let c = |v: f64| json!(designcraft_geom::format_measure(v, designcraft_geom::Unit::Points));
app.ui.dialog = Some(crate::dialogs::Dialog::new(
"fittingOptions",
json!({"autoFit": g.auto_fit != designcraft_doc::Fitting::None, "fitting": fitting, "align": g.fit_align,
"cropTop": c(g.crop[0]), "cropLeft": c(g.crop[1]), "cropBottom": c(g.crop[2]), "cropRight": c(g.crop[3])}),
));
Ok(Value::Null)
}
"app.userDictionary" => {
app.ui.dialog = Some(crate::dialogs::Dialog::new("userDictionary", json!({"word": ""})));
Ok(Value::Null)
+4 -4
View File
@@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Priority | Features | Done | Partial | Missing | Parity |
|---|---|---|---|---|---|
| P0 | 102 | 99 | 3 | 0 | 99% |
| P1 | 97 | 80 | 16 | 1 | 91% |
| P1 | 97 | 82 | 14 | 1 | 92% |
| P2 | 76 | 23 | 13 | 40 | 39% |
| Area | Parity |
@@ -18,7 +18,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
| Application shell & workspace | 80% |
| Documents, pages, spreads | 85% |
| Layers | 95% |
| Frames, shapes & paths | 93% |
| Frames, shapes & paths | 95% |
| Transform | 100% |
| Fill, stroke, colour | 93% |
| Effects & transparency | 71% |
@@ -109,9 +109,9 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
| Frames, shapes & paths | Text outlines | P1 | D |
| Frames, shapes & paths | Clipping paths | P2 | M |
| Frames, shapes & paths | Frame fitting | P0 | D |
| Frames, shapes & paths | Frame Fitting Options | P1 | P |
| Frames, shapes & paths | Frame Fitting Options | P1 | D |
| Frames, shapes & paths | Content grabber, select content/container | P0 | D |
| Frames, shapes & paths | Auto-fit | P1 | P |
| Frames, shapes & paths | Auto-fit | P1 | D |
| Frames, shapes & paths | Step and Repeat | P1 | D |
| Frames, shapes & paths | Gridify while drawing | P2 | M |
| Frames, shapes & paths | Gap tool | P2 | M |