diff --git a/crates/doc/src/text/para_style.rs b/crates/doc/src/text/para_style.rs index a257e2e0a..f189801bc 100644 --- a/crates/doc/src/text/para_style.rs +++ b/crates/doc/src/text/para_style.rs @@ -228,7 +228,8 @@ pub struct ParaStyle { pub leading_model: LeadingModel, /// Hanging punctuation (Paragraph panel menu › Burasagari): a comma or full stop ending a line /// may stand outside the frame. New type takes [`Burasagari::Standard`]; documents from before - /// it and imported text keep [`Burasagari::None`]. + /// it and imported text keep [`Burasagari::None`], except legacy `.ai` type with binding + /// punctuation on, which opens with [`Burasagari::Forced`]. pub burasagari: Burasagari, /// Kinsoku (Paragraph panel › Kinsoku Set): which Japanese characters may not start or end a /// line. diff --git a/crates/engine/tests/import_fuzz.rs b/crates/engine/tests/import_fuzz.rs index 1ffbbe7d8..05c538e12 100644 --- a/crates/engine/tests/import_fuzz.rs +++ b/crates/engine/tests/import_fuzz.rs @@ -4562,6 +4562,8 @@ fn arb_ai_token() -> impl Strategy { "1e308 Ty", "1 TY", "(x) TY", + "3 Tg", + "1e308 Tg", "1 1 1e308 -1e308 4e9 Th", "1 1 (2) 3 4 Th", "36 2 Xb", diff --git a/crates/eps/src/import/ai/legacy_text.rs b/crates/eps/src/import/ai/legacy_text.rs index 3141220aa..a1b24662e 100644 --- a/crates/eps/src/import/ai/legacy_text.rs +++ b/crates/eps/src/import/ai/legacy_text.rs @@ -3,8 +3,8 @@ //! type runs along, `TP`), the modal character and paragraph attributes //! (`Tr Tf Ts Tz Tt TA Tl Ta Ti`, and §15.4.7 and §22's letter and word spacing `TC TW`, hanging //! punctuation `Tq`, hyphenation `Th` and tab stops `Xb Xe XB`, and §15.3.4's writing direction `TV`, -//! Roman character rotation `Tv` and Kumi blocks `Ty TY`), the paint style, and the characters (`Tx`, `Tj`, -//! overflow `TX`; `\r` ends a paragraph). Type set in rendering modes 4 to 7 is a mask (§5.7): the +//! Roman character rotation `Tv`, Kumi blocks `Ty TY` and binding punctuation `Tg`), the paint +//! style, and the characters (`Tx`, `Tj`, overflow `TX`; `\r` ends a paragraph). Type set in rendering modes 4 to 7 is a mask (§5.7): the //! text object clips the objects of its clipping group. Final-form operators (the //! computed positions, §15.1) are skipped: the type is laid out again. Strings are decoded by the //! encoding pairs of the current font's `TZ`, then by the file's `TE` encoding unless that `TZ`'s @@ -17,7 +17,7 @@ use std::sync::Arc; use craft_pdf::interpret::hayro_cmap::{BfString, CMap, CMapName, load_embedded}; use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen}; use vectorcraft_color::Paint; -use vectorcraft_doc::{CharStyle, Justify, NodeKind, ParaStyle, TabAlign, TabStop, TextKind, TextObject, TextRun}; +use vectorcraft_doc::{Burasagari, CharStyle, Justify, NodeKind, ParaStyle, TabAlign, TabStop, TextKind, TextObject, TextRun}; use vectorcraft_geom::AnchorKind; use super::super::obj::V; @@ -870,6 +870,19 @@ impl Reader<'_> { st.para.roman_hanging = on; } } + // `bindrepeat Tg` (§15.3.4): 1 and 3 turn binding punctuation on, and 0 and 2 turn it + // off. VectorCraft's own mapping opens it on as Burasagari's Force setting and off as no + // Burasagari. The repeat character (2 and 3) is not kept. + "Tg" => { + let burasagari = match n0 { + Some(1.0 | 3.0) => Some(Burasagari::Forced), + Some(0.0 | 2.0) => Some(Burasagari::None), + _ => None, + }; + if let Some(b) = burasagari { + st.para.burasagari = b; + } + } // `hyphenate limitHyphLines minLeadHyphen minTailHyphen maxHyphLines Th`, kept to the // ranges `text.setFormat` takes. "Th" => { diff --git a/crates/eps/src/import/ai/read/ops.rs b/crates/eps/src/import/ai/read/ops.rs index 7564e0c2e..3d95c9485 100644 --- a/crates/eps/src/import/ai/read/ops.rs +++ b/crates/eps/src/import/ai/read/ops.rs @@ -360,13 +360,13 @@ impl<'a> Reader<'a> { "TO" => self.end_text(hidden)?, // `(text) justified TS`, special characters (§22.6): the reader adds its characters like `Tx`. "Tx" | "Tj" | "TX" | "TS" => self.text_body(w, &vals), - "Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" | "TC" | "TW" | "Tq" | "Th" | "TV" | "Tv" | "Ty" | "TY" | "Xb" | "Xe" - | "XB" => { + "Tr" | "Tf" | "Ts" | "Tz" | "Tt" | "TA" | "Tl" | "Ta" | "Ti" | "TC" | "TW" | "Tq" | "Th" | "TV" | "Tv" | "Ty" | "TY" | "Tg" | "Xb" + | "Xe" | "XB" => { self.text_attr(w, &vals); } - // Attributes the reader doesn't keep (kerning pairs, line breaking, binding punctuation - // and Japanese layout rules) and final-form positions (§15.3). - "Tk" | "TK" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TG" | "Tg" | "Tu" | "Xu" => {} + // Attributes the reader doesn't keep (kerning pairs, line breaking and Japanese layout + // rules) and final-form positions (§15.3). + "Tk" | "TK" | "Tc" | "Tw" | "Tm" | "Td" | "T*" | "TR" | "T+" | "T-" | "TG" | "Tu" | "Xu" => {} // `TM` (pop text matrix) and `Te` (end render), listed in §22.6, keep nothing either; the // reader takes `Te` with any number of operands. "TM" | "Te" => {} diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index b8cb12218..ad8833775 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -3599,6 +3599,32 @@ fn legacy_type_keeps_its_spacing_hanging_punctuation_and_hyphenation() { assert_eq!(texts[1].para_at(0), two); } +/// `Tg` (§15.3.4) turns binding punctuation on (1 and 3) or off (0 and 2); VectorCraft opens it on +/// as Burasagari's Force setting. Each paragraph of point type and area type keeps the value in +/// force at its first character, and it lasts into the next text object. +#[test] +fn legacy_type_keeps_its_binding_punctuation() { + use vectorcraft_doc::Burasagari; + let burasagari = |attrs: &str| legacy_texts(&point_type(attrs, "Hi"))[0].para_at(0).burasagari; + assert_eq!(burasagari(""), Burasagari::None); + for on in ["1", "3"] { + assert_eq!(burasagari(&format!("0 Tg\n{on} Tg")), Burasagari::Forced, "{on} Tg"); + } + for off in ["0", "2"] { + assert_eq!(burasagari(&format!("1 Tg\n{off} Tg")), Burasagari::None, "{off} Tg"); + } + let two_paragraphs = point_type("1 Tg", "One\\r").replace("TO\n", "0 Tg\n(Two) Tx\nTO\n"); + let texts = legacy_texts(&format!("{two_paragraphs}{}{}", point_type("3 Tg", "Three"), point_type("", "Four"))); + let each: Vec = vec![texts[0].para_at(0).burasagari, texts[0].para_at(1).burasagari, texts[1].para_at(0).burasagari]; + assert_eq!(each, [Burasagari::Forced, Burasagari::None, Burasagari::Forced]); + assert_eq!(texts[2].para_at(0).burasagari, Burasagari::Forced, "the next text object keeps it"); + let frame = "10 10 m\n110 10 L\n110 110 L\n10 110 L\nN"; + let area = format!("1 To\n1 0 0 1 10 110 0 Tp\n{frame}\nTP\n0 Tr\n0 0 0 1 k\n/_Helvetica 12 Tf\n1 Tg\n(Area) Tx\nTO\n"); + let texts = legacy_texts(&area); + assert!(matches!(texts[0].kind, vectorcraft_doc::TextKind::Area { .. })); + assert_eq!(texts[0].para_at(0).burasagari, Burasagari::Forced, "area type"); +} + /// `TV` (§15.3.4) sets the writing direction of point type, area type, type on a path and linked /// areas, and lasts into the next text object until it changes. #[test] @@ -3739,6 +3765,10 @@ fn legacy_type_attributes_with_bad_operands_stay_as_they_were() { assert_eq!(p.justify, vectorcraft_doc::Justify::Right); assert_eq!((p.first_line_indent, p.left_indent, p.right_indent), (5.0, 6.0, 7.0)); assert_eq!(texts[0].runs[0].style.kerning, Some(0.0), "kerning stays off"); + let texts = legacy_texts(&point_type("1 Tg\n4 Tg\n-1 Tg\n0.5 Tg\n/x Tg\n(0) Tg\nTg", "Hi")); + assert_eq!(texts[0].para_at(0).burasagari, vectorcraft_doc::Burasagari::Forced, "binding punctuation stays on"); + let texts = legacy_texts(&point_type("0 Tg\n4 Tg\n-1 Tg\n2.5 Tg", "Hi")); + assert_eq!(texts[0].para_at(0).burasagari, vectorcraft_doc::Burasagari::None, "binding punctuation stays off"); let texts = legacy_texts(&point_type("1 TV\n1 Tv\n1 Ty\n2 TV\n/x TV\nTV\n-1 Tv\n(x) Tv\n0.5 Ty\n2 TY", "Hi")); let st = &texts[0].runs[0].style; assert!(texts[0].vertical && st.standard_vertical_roman && st.tate_chu_yoko); diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 7579eceab..fd68d9748 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -187,7 +187,9 @@ private editing data. VectorCraft: block in vertical type opens unmarked, and two blocks with nothing between them open as one block. Its upright Roman characters (`Tv`) open with Standard Vertical Roman Alignment. VectorCraft's own layout rule also sets a run of two or three half-width digits across the column in vertical type, inside or outside a block and with - either `Tv` value. + either `Tv` value. Its binding punctuation (`Tg`) opens as Burasagari's Force setting when on and as no + Burasagari when off, which is VectorCraft's own mapping; the repeat character, line breaking (`TG`) and + Japanese layout rules (`Xu`, `Tu`) are not read. Legacy `.ai` type set in rendering modes 4 to 7 (`Tr`, text used as a mask) is a clipping path of its clipping group, as a `W` path is, and the mode lasts into later text objects. Type in linked areas, in wraparound text or in a compound path doesn't clip, with a warning. A `Tr` operand that is not a whole number diff --git a/docs/mcp.md b/docs/mcp.md index 646204cc7..196488a06 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5529,8 +5529,8 @@ Force) of the selected type (or `ids`): an East Asian comma or full stop ending 、。) hangs outside the frame, Regular only when it doesn't fit, Force always (the rest of the line then fills the measure). Closing brackets and Latin punctuation don't hang; point type, right-to-left and hyphenated lines are left as they are. The hanging mark is as wide as its punctuation spacing makes it (`mojikumi`). New type (the Type tools, -`text.create`) takes `standard`; documents from before it and imported text keep `none`, and `none` isn't saved. One -undo step. +`text.create`) takes `standard`; documents from before it and imported text keep `none`, except legacy `.ai` type +with binding punctuation (`Tg`) on, which opens as `forced`. `none` isn't saved. One undo step. ```json {"name":"run_command","arguments":{"command":"text.setFormat","params":{"burasagari":"forced"}}}