mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
P: live captions that follow their source's metadata
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7acccc354c
commit
84476ffef4
7 files changed
+81
-8
No files matched your search
@@ -109,6 +109,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
||||
- Effects: Inner Glow (edge or centre), Bevel and Emboss (inner bevel lit from the light angle), Satin (`object.innerGlow`, `object.bevel`, `object.satin`); PDF export now keeps soft effects: objects with shadows, glows, feathers, bevels or satin go out as 300 ppi transparent images of their appearance (threaded text frames excepted).
|
||||
- Directional Feather (`object.directionalFeather {widths: [top, left, bottom, right]}`): each side fades over its own width.
|
||||
- View ▸ Flattener Preview (`view.flattenerPreview`): objects involving transparency highlighted in red; knockout groups export as images (PDF output has no knockout groups).
|
||||
- Live captions (`object.caption {live: true}`, Object ▸ Captions ▸ Generate Live Caption): the caption text is re-filled from the source object's metadata after every edit (same undo step).
|
||||
- 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.
|
||||
|
||||
@@ -467,6 +467,14 @@ pub struct ObjectLiquid {
|
||||
pub pin_right: bool,
|
||||
}
|
||||
|
||||
/// The source and template of a live caption.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LiveCaption {
|
||||
pub source: ItemId,
|
||||
pub template: String,
|
||||
}
|
||||
|
||||
/// Media panel options for a placed video or sound.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
@@ -908,6 +916,10 @@ pub struct Item {
|
||||
/// Liquid Layout (object-based pages): pins and resize permissions.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub liquid: Option<ObjectLiquid>,
|
||||
/// Live caption: this text frame shows `template` filled from the source object's metadata,
|
||||
/// kept current as the source changes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub live_caption: Option<LiveCaption>,
|
||||
/// Object › Object Layer Options: layers of a placed PDF that are hidden.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub pdf_hidden_layers: Vec<String>,
|
||||
@@ -967,6 +979,7 @@ impl Item {
|
||||
xml_tag: String::new(),
|
||||
button: None,
|
||||
pdf_hidden_layers: Vec::new(),
|
||||
live_caption: None,
|
||||
media: None,
|
||||
liquid: None,
|
||||
states: Vec::new(),
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Generate Static Caption",
|
||||
["Object", "Captions"],
|
||||
None,
|
||||
"{text?: template (default \"{name}\"; variables {name} {path} {altText} {label} {ppi} {dimensions} {format}), position?: below|above|left|right, offset? (pt, 0), height? (pt, 24), style?: paragraph style, ids?} → {ids}",
|
||||
"{text?: template (default \"{name}\"; variables {name} {path} {altText} {label} {ppi} {dimensions} {format}), position?: below|above|left|right, offset? (pt, 0), height? (pt, 24), style?: paragraph style, live?: bool (kept current as the source changes), ids?} → {ids}",
|
||||
has_selection,
|
||||
caption
|
||||
)]
|
||||
@@ -44,7 +44,7 @@ fn variable(d: &Document, it: &Item, xf: designcraft_geom::Affine, name: &str) -
|
||||
}
|
||||
}
|
||||
|
||||
fn fill(d: &Document, it: &Item, xf: designcraft_geom::Affine, template: &str) -> String {
|
||||
pub(crate) fn fill(d: &Document, it: &Item, xf: designcraft_geom::Affine, template: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut rest = template;
|
||||
while let Some(i) = rest.find('{') {
|
||||
@@ -75,6 +75,7 @@ fn caption(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let offset = p.get("offset").and_then(Value::as_f64).unwrap_or(0.0);
|
||||
let height = p.get("height").and_then(Value::as_f64).unwrap_or(24.0).max(1.0);
|
||||
let style = str_param(p, "style").map(str::to_string);
|
||||
let live = p.get("live").and_then(Value::as_bool).unwrap_or(false);
|
||||
if let Some(st) = &style
|
||||
&& s.doc()?.doc.styles.para(st).is_none()
|
||||
{
|
||||
@@ -96,7 +97,11 @@ fn caption(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => Rect::new(b.x0, b.y1 + offset, b.x1, b.y1 + offset + height),
|
||||
};
|
||||
let para = ParaFormat { style: style.clone().unwrap_or_else(|| d.styles.default_paragraph.clone()), ..Default::default() };
|
||||
let source = *id;
|
||||
let (cid, _) = d.add_text_frame(loc.spread, r, lid, &text, para)?;
|
||||
if live && let Some(c) = d.item_mut(cid) {
|
||||
c.live_caption = Some(designcraft_doc::LiveCaption { source, template: template.clone() });
|
||||
}
|
||||
made.push(cid);
|
||||
}
|
||||
if made.is_empty() {
|
||||
@@ -107,6 +112,29 @@ fn caption(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Bring live captions up to date with their source objects (the text frame's story is
|
||||
/// replaced when its text changed; a caption whose source is gone keeps its last text).
|
||||
pub(crate) fn refresh_live(d: &mut Document) -> bool {
|
||||
let mut todo = Vec::new();
|
||||
for id in d.all_items() {
|
||||
let Some(c) = d.item(id) else { continue };
|
||||
let (Some(lc), Some(tf)) = (&c.live_caption, c.text_frame()) else { continue };
|
||||
let (Some(loc), Some(src)) = (d.find(lc.source), d.item(lc.source)) else { continue };
|
||||
let text = fill(d, src, d.parent_xf(&loc) * src.xf, &lc.template);
|
||||
if d.story(tf.story).is_some_and(|st| st.text != text) {
|
||||
todo.push((tf.story, text));
|
||||
}
|
||||
}
|
||||
let changed = !todo.is_empty();
|
||||
for (sid, text) in todo {
|
||||
if let Some(st) = d.story_mut(sid) {
|
||||
let len = st.len();
|
||||
st.replace(0..len, &text);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -130,4 +158,23 @@ mod tests {
|
||||
assert_eq!(d.stories[&sid].text, "Figure fig-1: Harbour at dawn");
|
||||
assert!(s.execute("object.caption", &json!({"ids": [a], "position": "sideways"})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_caption_follows_its_source() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let a = s.execute("frame.create", &json!({"rect": [72, 72, 272, 172]})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("object.altText", &json!({"text": "First", "ids": [a]})).unwrap();
|
||||
let r = s.execute("object.caption", &json!({"ids": [a], "text": "Photo: {altText}", "live": true})).unwrap();
|
||||
let cid = designcraft_doc::ItemId(r["ids"][0].as_u64().unwrap());
|
||||
let text = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
d.stories[&d.item(cid).unwrap().text_frame().unwrap().story].text.clone()
|
||||
};
|
||||
assert_eq!(text(&s), "Photo: First");
|
||||
s.execute("object.altText", &json!({"text": "Second", "ids": [a]})).unwrap();
|
||||
assert_eq!(text(&s), "Photo: Second", "updated with the source");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(text(&s), "Photo: First", "one undo step for both");
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ pub mod anchors;
|
||||
mod articles;
|
||||
pub mod book;
|
||||
mod buttons;
|
||||
mod captions;
|
||||
pub(crate) mod captions;
|
||||
mod changes;
|
||||
mod colorsettings;
|
||||
mod conditions;
|
||||
|
||||
@@ -348,6 +348,17 @@ impl Session {
|
||||
if self.prefs.smart_text_reflow && spec.undoable {
|
||||
self.smart_reflow();
|
||||
}
|
||||
// Live captions follow their sources.
|
||||
if spec.undoable
|
||||
&& let Some(st) = self.active_mut()
|
||||
&& st.interaction.is_none()
|
||||
&& st.doc.spreads.iter().any(|sp| sp.items.iter().any(|i| i.live_caption.is_some()))
|
||||
{
|
||||
let mut d = (*st.doc).clone();
|
||||
if cmd::captions::refresh_live(&mut d) {
|
||||
st.doc = Arc::new(d);
|
||||
}
|
||||
}
|
||||
// Record undo if the document changed (and we're not previewing an interaction).
|
||||
if let (Some((uid, old)), Some(st)) = (before, self.active_mut())
|
||||
&& st.uid == uid
|
||||
|
||||
@@ -483,6 +483,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
||||
"<",
|
||||
">Captions",
|
||||
"cmd:object.caption",
|
||||
"cmd:object.caption|Generate Live Caption|{\"live\": true}",
|
||||
"<",
|
||||
">Anchored Object",
|
||||
"cmd:anchored.options",
|
||||
|
||||
+5
-5
@@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 100 | 2 | 0 | 99% |
|
||||
| P1 | 97 | 94 | 3 | 0 | 98% |
|
||||
| P2 | 76 | 51 | 25 | 0 | 84% |
|
||||
| P2 | 76 | 53 | 23 | 0 | 85% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
@@ -22,12 +22,12 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 99% |
|
||||
| Effects & transparency | 92% |
|
||||
| Placing & links | 91% |
|
||||
| Placing & links | 93% |
|
||||
| Type & text frames | 99% |
|
||||
| Typography | 98% |
|
||||
| Styles | 97% |
|
||||
| Tables | 98% |
|
||||
| Long documents | 96% |
|
||||
| Long documents | 100% |
|
||||
| Interactivity & digital | 90% |
|
||||
| Output & production | 82% |
|
||||
| XML & automation | 83% |
|
||||
@@ -176,7 +176,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| Placing & links | Effective/actual PPI | P0 | D |
|
||||
| Placing & links | Relink to folder / extension | P1 | D |
|
||||
| Placing & links | Copy Links To / Package | P1 | D |
|
||||
| Placing & links | Captions | P2 | P |
|
||||
| Placing & links | Captions | P2 | D |
|
||||
| Placing & links | Image import options | P1 | D |
|
||||
| Placing & links | Linked text/stories | P2 | D |
|
||||
| Placing & links | Display performance | P0 | D |
|
||||
@@ -267,7 +267,7 @@ Open P0 items: Vector/page formats (PDF/AI/SVG/EPS place), Export to PDF (print)
|
||||
| Long documents | Cross-references | P2 | D |
|
||||
| Long documents | Running headers via variables | P1 | D |
|
||||
| Long documents | Footnotes/endnotes | P1 | D |
|
||||
| Long documents | Captions | P2 | P |
|
||||
| Long documents | Captions | P2 | D |
|
||||
| Interactivity & digital | Hyperlinks panel | P1 | D |
|
||||
| Interactivity & digital | Bookmarks | P1 | D |
|
||||
| Interactivity & digital | Buttons and Forms | P2 | P |
|
||||
|
||||
Reference in new issue
Block a user