mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7827 from dotCooCoo/feat/ai-export-writes-kinsoku-kinsoku-shori-type
.ai export: paragraphs keep Kinsoku, Kinsoku Shori Type, Burasagari and Bunri-Kinshi
This commit is contained in:
@@ -4108,20 +4108,21 @@ fn predefined_mojikumi_sets_are_reported_as_left_out() {
|
||||
}
|
||||
}
|
||||
|
||||
/// The text document has no Bunri-Kinshi: a paragraph with it on and an inseparable pair in it is
|
||||
/// reported as left out, and comes back off; with it off, or with no pair, nothing is reported.
|
||||
/// Bunri-Kinshi comes back on or off, with or without an inseparable pair in the paragraph, and
|
||||
/// nothing is reported.
|
||||
#[test]
|
||||
fn bunri_kinshi_is_reported_only_for_paragraphs_with_a_pair() {
|
||||
fn bunri_kinshi_comes_back_with_or_without_a_pair() {
|
||||
use vectorcraft_doc::TextObject;
|
||||
for (text, on, reported) in [
|
||||
("Wait——now", true, true),
|
||||
("Wait——now", false, false),
|
||||
("Wait—now", true, false),
|
||||
("Wait……now", true, true),
|
||||
("Wait‥‥now", true, true),
|
||||
("あ〳〵い", true, true),
|
||||
("あ〴〵い", true, true),
|
||||
("あ〵〳い", true, false),
|
||||
for (text, on) in [
|
||||
("Wait——now", true),
|
||||
("Wait——now", false),
|
||||
("Wait—now", true),
|
||||
("Wait……now", true),
|
||||
("Wait‥‥now", true),
|
||||
("あ〳〵い", true),
|
||||
("あ〴〵い", true),
|
||||
("あ〵〳い", true),
|
||||
("あ〵〳い", false),
|
||||
] {
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), text, style(14.0, Color::BLACK));
|
||||
t.para.bunri_kinshi = on;
|
||||
@@ -4130,7 +4131,7 @@ fn bunri_kinshi_is_reported_only_for_paragraphs_with_a_pair() {
|
||||
let n = text_node(&mut doc, t);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert_eq!(!out.warnings.is_empty(), reported, "{text} {on}: {:?}", out.warnings);
|
||||
assert!(out.warnings.is_empty(), "{text} {on}: {:?}", out.warnings);
|
||||
let mut all = vec![];
|
||||
paths(&read_back(&doc).layers, &mut all);
|
||||
let paras: Vec<bool> = all
|
||||
@@ -4140,10 +4141,87 @@ fn bunri_kinshi_is_reported_only_for_paragraphs_with_a_pair() {
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(paras, [false], "{text} {on}");
|
||||
assert_eq!(paras, [on], "{text} {on}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Each paragraph's Kinsoku set, Kinsoku Shori Type, Burasagari and Bunri-Kinshi come back, and
|
||||
/// nothing is reported: every value of each, mixed across the paragraphs of one object, Burasagari
|
||||
/// and Bunri-Kinshi kept under Kinsoku None. The text document lists the Hard and Soft sets, and
|
||||
/// every paragraph, the normal paragraph style and the document's defaults write all four keys.
|
||||
/// Another attribute the text document leaves out is still reported.
|
||||
#[test]
|
||||
fn kinsoku_settings_come_back_on_each_paragraph() {
|
||||
use vectorcraft_doc::{Burasagari, Kinsoku, KinsokuShoriType, ParaStyle, TextObject};
|
||||
let cases = [
|
||||
(Kinsoku::None, KinsokuShoriType::PushInFirst, Burasagari::Forced, true),
|
||||
(Kinsoku::Soft, KinsokuShoriType::PushOutFirst, Burasagari::Standard, false),
|
||||
(Kinsoku::Hard, KinsokuShoriType::PushOutOnly, Burasagari::None, true),
|
||||
(Kinsoku::Hard, KinsokuShoriType::PushInFirst, Burasagari::Standard, false),
|
||||
(Kinsoku::Soft, KinsokuShoriType::PushOutOnly, Burasagari::Forced, true),
|
||||
];
|
||||
let mut t = TextObject::point(vectorcraft_geom::Point::new(20.0, 40.0), "かな。\n「かな」、\nあ——い\nかな\nかな。", style(14.0, Color::BLACK));
|
||||
t.set_paragraph_styles(
|
||||
cases
|
||||
.iter()
|
||||
.map(|&(kinsoku, kinsoku_type, burasagari, bunri_kinshi)| ParaStyle {
|
||||
kinsoku,
|
||||
kinsoku_type,
|
||||
burasagari,
|
||||
bunri_kinshi,
|
||||
..ParaStyle::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let mut doc = Document::new(200.0, 200.0);
|
||||
let layer = doc.layers[0].id;
|
||||
let n = text_node(&mut doc, t);
|
||||
doc.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
let out = export_ai_editing(&doc, &opts()).unwrap();
|
||||
assert!(out.warnings.is_empty(), "{:?}", out.warnings);
|
||||
let back = read_back(&doc);
|
||||
same_lines(&type_lines(&back), &type_lines(&doc));
|
||||
let mut all = vec![];
|
||||
paths(&back.layers, &mut all);
|
||||
let read: Vec<_> = all
|
||||
.iter()
|
||||
.filter_map(|n| match &n.kind {
|
||||
NodeKind::Text(t) => {
|
||||
Some(t.paragraph_styles().iter().map(|p| (p.kinsoku, p.kinsoku_type, p.burasagari, p.bunri_kinshi)).collect::<Vec<_>>())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(read, [cases.to_vec()]);
|
||||
let decoded = text_document(&program(&doc));
|
||||
let hard = decoded.find("PhotoshopKinsokuHard").unwrap_or_else(|| panic!("{decoded}"));
|
||||
let soft = decoded.find("PhotoshopKinsokuSoft").unwrap_or_else(|| panic!("{decoded}"));
|
||||
assert!(hard < soft, "{decoded}");
|
||||
// Five paragraphs, then the normal paragraph style and the document's defaults (Hard, Push Out
|
||||
// Only, no Burasagari, Bunri-Kinshi off).
|
||||
for (key, count) in [
|
||||
("/27 /nil", 1),
|
||||
("/27 0", 4),
|
||||
("/27 1", 2),
|
||||
("/25 0", 2),
|
||||
("/25 1", 1),
|
||||
("/25 2", 4),
|
||||
("/24 0", 3),
|
||||
("/24 1", 2),
|
||||
("/24 2", 2),
|
||||
("/23 true", 3),
|
||||
("/23 false", 4),
|
||||
] {
|
||||
assert_eq!(decoded.matches(key).count(), count, "{key}: {decoded}");
|
||||
}
|
||||
let mut lossy = doc.clone();
|
||||
let id = lossy.layers[0].children().unwrap()[0].id;
|
||||
if let Some(NodeKind::Text(t)) = lossy.node_mut(id).map(|n| &mut n.kind) {
|
||||
t.runs[0].style.underline = true;
|
||||
}
|
||||
assert_eq!(export_ai_editing(&lossy, &opts()).unwrap().warnings, [writer::TYPE_ATTRIBUTES]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_is_written_again_as_the_same_bytes() {
|
||||
let doc = typed();
|
||||
|
||||
@@ -22,15 +22,15 @@ use kurbo::{ParamCurveArclen, PathSeg};
|
||||
use vectorcraft_color::{Color, Paint};
|
||||
use vectorcraft_doc::text::MAX_AKI;
|
||||
use vectorcraft_doc::{
|
||||
AreaOptions, Burasagari, CharStyle, Document, FirstBaseline, Justify, LineCap, LineJoin, Mojikumi, Node, NodeId, NodeKind, ParaDirection,
|
||||
ParaStyle, PathAlign, PathEffect, TextKind, TextObject, VerticalAlign,
|
||||
AreaOptions, Burasagari, CharStyle, Document, FirstBaseline, Justify, Kinsoku, KinsokuShoriType, LineCap, LineJoin, Mojikumi, Node, NodeId,
|
||||
NodeKind, ParaDirection, ParaStyle, PathAlign, PathEffect, TextKind, TextObject, VerticalAlign,
|
||||
};
|
||||
use vectorcraft_geom::{Affine, PathData, Point};
|
||||
|
||||
use crate::ps;
|
||||
|
||||
/// What the text document holds of type, and what it leaves out.
|
||||
pub(in crate::ai_editing) const TYPE_ATTRIBUTES: &str = "attributes of type other than its font, size, leading, tracking, baseline shift, horizontal and vertical scale, solid fill, solid stroke and its weight, cap, join and miter limit, capitals, small capitals at the document's size, Tate-chu-yoko, Standard Vertical Roman Alignment, Insert Aki, paragraph alignment, indents, space before and after, hyphenation on or off and right-to-left direction (for example underline, kerning, OpenType features, swatch links of its colors, character and paragraph styles, tabs, lists, optical margin alignment, area type's inset spacing, vertical alignment and flow, a minimum first baseline with the Ascent first baseline, and type on a path's effect, alignment and spacing and an end bracket at the very end of an open path) are left out of the editing data for other apps";
|
||||
pub(in crate::ai_editing) const TYPE_ATTRIBUTES: &str = "attributes of type other than its font, size, leading, tracking, baseline shift, horizontal and vertical scale, solid fill, solid stroke and its weight, cap, join and miter limit, capitals, small capitals at the document's size, Tate-chu-yoko, Standard Vertical Roman Alignment, Insert Aki, paragraph alignment, indents, space before and after, hyphenation on or off, right-to-left direction, Kinsoku set, Kinsoku Shori Type, Burasagari and Bunri-Kinshi (for example underline, kerning, OpenType features, swatch links of its colors, character and paragraph styles, tabs, lists, optical margin alignment, area type's inset spacing, vertical alignment and flow, a minimum first baseline with the Ascent first baseline, and type on a path's effect, alignment and spacing and an end bracket at the very end of an open path) are left out of the editing data for other apps";
|
||||
|
||||
/// The version of the text document's form (`/98 << /0 n >>`), as the newest files users sent have it.
|
||||
const STREAM_VERSION: u32 = 14;
|
||||
@@ -161,6 +161,10 @@ fn dict(pairs: Vec<(u32, V)>) -> V {
|
||||
V::Dict(pairs)
|
||||
}
|
||||
|
||||
/// The document's Kinsoku sets, in order, each by its name: a paragraph's key 27 is its set's index
|
||||
/// here (0 Hard, 1 Soft), as the reader's `kinsoku_sets` reads them.
|
||||
const KINSOKU_SETS: [&str; 2] = ["PhotoshopKinsokuHard", "PhotoshopKinsokuSoft"];
|
||||
|
||||
/// A resource set: its resources, each `<< /0 resource >>`, and its display list.
|
||||
fn set(resources: Vec<V>, display: bool) -> V {
|
||||
let mut d = vec![(0, V::List(resources.into_iter().map(|r| dict(vec![(0, r)])).collect()))];
|
||||
@@ -401,7 +405,6 @@ pub(in crate::ai_editing) fn loses(t: &TextObject) -> bool {
|
||||
}
|
||||
let hangul =
|
||||
text.chars().any(|c| matches!(c, '\u{1100}'..='\u{11FF}' | '\u{3130}'..='\u{318F}' | '\u{A960}'..='\u{A97F}' | '\u{AC00}'..='\u{D7FF}' | '\u{FFA0}'..='\u{FFDC}'));
|
||||
let pairs = text.chars().zip(text.chars().skip(1)).any(|(a, b)| vectorcraft_text::inseparable(a, b));
|
||||
let held = ParaStyle {
|
||||
justify: p.justify,
|
||||
left_indent: p.left_indent,
|
||||
@@ -411,16 +414,16 @@ pub(in crate::ai_editing) fn loses(t: &TextObject) -> bool {
|
||||
space_after: p.space_after,
|
||||
hyphenate: p.hyphenate,
|
||||
direction: p.direction.filter(|d| *d == ParaDirection::RightToLeft),
|
||||
// The composition of Japanese punctuation new type gets (Line-end Punctuation Half
|
||||
// Width, Standard burasagari) isn't reported: it changes no other script's lines.
|
||||
// The Mojikumi set new type gets (Line-end Punctuation Half Width) isn't reported: it
|
||||
// changes no other script's lines.
|
||||
mojikumi: if p.mojikumi == Mojikumi::LineEndHalf { p.mojikumi } else { ParaStyle::default().mojikumi },
|
||||
burasagari: if p.burasagari == Burasagari::Standard { p.burasagari } else { ParaStyle::default().burasagari },
|
||||
kinsoku: p.kinsoku,
|
||||
kinsoku_type: p.kinsoku_type,
|
||||
burasagari: p.burasagari,
|
||||
bunri_kinshi: p.bunri_kinshi,
|
||||
// The text document has no Korean Line Break Anywhere: it reads back off, which
|
||||
// changes only the lines of a paragraph with Hangul in it.
|
||||
korean_line_break_anywhere: p.korean_line_break_anywhere && !hangul,
|
||||
// Nor Bunri-Kinshi: it reads back off, which changes only the lines of a paragraph
|
||||
// with an inseparable pair in it.
|
||||
bunri_kinshi: p.bunri_kinshi && !pairs,
|
||||
..ParaStyle::default()
|
||||
};
|
||||
*p != held
|
||||
@@ -756,6 +759,32 @@ impl TextDoc {
|
||||
(4, len(p.space_before)),
|
||||
(5, len(p.space_after)),
|
||||
(9, V::Bool(p.hyphenate)),
|
||||
(23, V::Bool(p.bunri_kinshi)),
|
||||
(
|
||||
24,
|
||||
V::Int(match p.burasagari {
|
||||
Burasagari::None => 0,
|
||||
Burasagari::Standard => 1,
|
||||
Burasagari::Forced => 2,
|
||||
}),
|
||||
),
|
||||
(
|
||||
25,
|
||||
V::Int(match p.kinsoku_type {
|
||||
KinsokuShoriType::PushInFirst => 0,
|
||||
KinsokuShoriType::PushOutFirst => 1,
|
||||
KinsokuShoriType::PushOutOnly => 2,
|
||||
}),
|
||||
),
|
||||
// The Kinsoku set: `/nil` for None, else its index in the document's sets ([`KINSOKU_SETS`]).
|
||||
(
|
||||
27,
|
||||
match p.kinsoku {
|
||||
Kinsoku::None => V::Name("nil"),
|
||||
Kinsoku::Hard => V::Int(0),
|
||||
Kinsoku::Soft => V::Int(1),
|
||||
},
|
||||
),
|
||||
];
|
||||
if rtl {
|
||||
d.push((33, V::Int(1)));
|
||||
@@ -1110,7 +1139,7 @@ impl TextDoc {
|
||||
(1, set(fonts, true)),
|
||||
(2, empty()),
|
||||
(3, empty()),
|
||||
(4, empty()),
|
||||
(4, set(KINSOKU_SETS.iter().map(|name| dict(vec![(0, V::Str((*name).into()))])).collect(), false)),
|
||||
(5, set(vec![normal_char], true)),
|
||||
(6, set(vec![normal_para], true)),
|
||||
(8, set(self.frames.iter().map(clone_v).collect(), false)),
|
||||
|
||||
+1
-1
@@ -9444,7 +9444,7 @@ a pair. The setting works only with a Kinsoku Set: with Kinsoku Set None the par
|
||||
and the menu item is unavailable. Off by default, for new type and for paragraphs saved before the option. It is saved
|
||||
with the paragraph (`bunri_kinshi` in `para`) only when on, paragraph styles carry it (Paragraph Style Options › East
|
||||
Asian Composition Settings), and Reset Panel turns it off. A value that is not true or false is refused. One undo
|
||||
step. `.ai` editing data does not hold it, and a save reports a paragraph with it on and a pair in it as left out. The help page:
|
||||
step. An `.ai` save writes it with each paragraph, under Kinsoku Set None too, and it reopens as set. The help page:
|
||||
https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html
|
||||
|
||||
```json
|
||||
|
||||
@@ -1460,7 +1460,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Type on a path with right-to-left text that does not fit shows the longest start of its text whose glyphs fit, also where tracking gives a letter a negative advance and a shorter start does not fit; the rest overflows or flows to the next threaded object (VectorCraft's own rule). Without hanging punctuation, and on a straight path or with no spacing, every start is measured without arranging it on the path again. With hanging punctuation, or spacing on a curve, when a letter has a negative advance, at most 32 of the starts that could fit are arranged one by one from the longest; when none of them fits, a binary search over the rest shows a start that fits, which is not always the longest (`overflowing_rtl_type_on_a_path_shows_the_longest_start_that_fits`, `the_longest_rtl_start_on_a_path_is_found_without_arranging_each_start`, `the_longest_rtl_start_on_a_path_is_the_one_each_start_gives` and `rtl_type_on_a_curve_whose_letters_take_the_line_back_is_arranged_a_bounded_number_of_times` in `crates/text/src/tests/bidi.rs`). Right-aligned and centered type on a curve with Spacing is placed by what its glyphs take where the line ends up, so text that fits is not cut off at the end bracket (`on_path_aligned_spaced_text_that_fits_shows_whole` in `crates/text/src/tests.rs`).
|
||||
The Paragraph panel menu's Kinsoku Shori Type (`text.setFormat {kinsokuType}`, with the East Asian options) picks Push In First, Push Out First or Push Out Only for each paragraph: a character the Kinsoku Set keeps off a line's start or end is set on the line, with the line's gaps next to CJK characters closed by at most a tenth of an em each to fit, or moved to the next line with the character before it. Push Out First pushes in only when pushing out would leave the line more than an em short. Push Out Only is the default, the setting is saved only when it is not, and paragraph styles carry it.
|
||||
The Paragraph panel menu's Bunri-Kinshi (`text.setFormat {bunriKinshi}`, with the East Asian options, and the East Asian Composition Settings section of Paragraph Style Options) keeps inseparable pairs on one line in each paragraph that has it: two em dashes, two ellipses, two two-dot leaders and the two halves of a vertical kana repeat mark (the pairs of W3C JLREQ's class cl-08, VectorCraft's set). Both composers and vertical type follow it, and justification adds no space inside a pair. It works with a Kinsoku Set only, is off by default and is saved only when on; editing the set of pairs is not available.
|
||||
`.ai` import reads each paragraph's Kinsoku Set (None, Hard or Soft), Kinsoku Shori Type, Burasagari and Bunri-Kinshi from the file's editable text: the paragraph's own setting, else its paragraph style's or a style that one is based on, else the document's default. A paragraph with a custom Kinsoku set takes its paragraph style's set, and the importer warns about it. A value it cannot read takes its paragraph style's value too. `.ai` export does not write these settings.
|
||||
`.ai` import reads each paragraph's Kinsoku Set (None, Hard or Soft), Kinsoku Shori Type, Burasagari and Bunri-Kinshi from the file's editable text: the paragraph's own setting, else its paragraph style's or a style that one is based on, else the document's default. A paragraph with a custom Kinsoku set takes its paragraph style's set, and the importer warns about it. A value it cannot read takes its paragraph style's value too. An `.ai` save writes all four settings for every paragraph, the normal paragraph style and the document's defaults, and lists the Hard and Soft sets, in the form that reader reads, so they reopen as set and are not reported as left out; Burasagari and Bunri-Kinshi keep their values under Kinsoku None.
|
||||
The Paragraph panel menu's Korean Line Break Anywhere (`text.setFormat {koreanLineBreakAnywhere}`, with the East Asian options, and the East Asian Composition Settings section of Paragraph Style Options) sets for each paragraph whether a line may break between any two Hangul syllables. When it is off, a word that contains Hangul breaks only at its spaces and after punctuation, or where the word is wider than the line, and takes no hyphen. New type has it off; paragraphs saved before the option read as on, so their lines do not move.
|
||||
The Paragraph panel menu's Reset Panel (`text.setStyle`, `text.setFormat` and `text.tabs.clear`) gives the selected type's paragraphs, or the paragraphs the Type tool's selection touches, Auto alignment, no indents or paragraph spacing, Hyphenate off with the default Hyphenation and Justification options, Auto direction, Bottom-to-Bottom Leading, the Line-end Punctuation Half Width Mojikumi Set, Regular burasagari, the Hard kinsoku set, Push Out Only, the Every-line Composer, Bunri-Kinshi, Roman Hanging Punctuation and Korean Line Break Anywhere off, no kashidas and no tab stops, as one undo step.
|
||||
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
|
||||
|
||||
Reference in New Issue
Block a user