mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
PDF: list markers export as selectable text
Squashed from #4098 onto main.
This commit is contained in:
+76
-16
@@ -21,8 +21,8 @@ use vectorcraft_color::{BlendMode, Color, GradientKind, Paint};
|
||||
use vectorcraft_doc::{AppearanceItem, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, SymbolBoxes, TextObject};
|
||||
use vectorcraft_effects::stroke::{self, WrittenShape};
|
||||
use vectorcraft_geom::{Affine, BezPath, FillRule, Rect};
|
||||
use vectorcraft_text::TextLayout;
|
||||
use vectorcraft_text::embed::Embedding;
|
||||
use vectorcraft_text::{ListMarker, TextLayout};
|
||||
|
||||
use crate::forms::Mark;
|
||||
use crate::lab_spot::{find, rfind};
|
||||
@@ -694,8 +694,11 @@ struct GlyphLine {
|
||||
/// The glyph space (krilla's, y down, at `size`) → text space, at the line's start.
|
||||
frame: Affine,
|
||||
/// Glyph id, pen position and advance along the line (in glyph space), and the characters it
|
||||
/// stands for (bytes of the plain text).
|
||||
/// stands for (bytes of the plain text, or of the list marker's text).
|
||||
glyphs: Vec<(u32, f64, f64, std::ops::Range<usize>)>,
|
||||
/// The list marker the glyphs belong to, by its first glyph's index in the layout
|
||||
/// ([`TextLayout::marker_of`]); `None` for glyphs of the plain text.
|
||||
marker: Option<usize>,
|
||||
}
|
||||
|
||||
/// Most pixels along a side of a freeform gradient's image (its colour field is smooth).
|
||||
@@ -1780,7 +1783,9 @@ impl Exporter<'_> {
|
||||
/// COLR, SVG), with `images`, are written as text too, which draws their images or layers; the
|
||||
/// indices of those it can't write or draw (SVG glyphs) are returned with the outlines. The
|
||||
/// hyphen the layout adds where it breaks a line inside a word ([`break_hyphen`]) is written
|
||||
/// as the character its glyph stands for, on its own after the glyphs before it.
|
||||
/// as the character its glyph stands for, on its own after the glyphs before it. The glyphs of
|
||||
/// a list marker are written as the marker's characters ([`marker_chars`]), apart from the
|
||||
/// plain text's glyphs; a marker glyph that stands for none of them is drawn as a path.
|
||||
///
|
||||
/// `bent`: `Some(false)` writes the glyphs bent along a path ([`vectorcraft_text::GlyphBend`])
|
||||
/// as outlines, not text (text is set straight); `Some(true)` writes only those, as text where
|
||||
@@ -1802,6 +1807,11 @@ impl Exporter<'_> {
|
||||
// stand for no character of it, the characters a line-break hyphen can stand for.
|
||||
let written: Cow<str> =
|
||||
if layout.glyphs.iter().any(|g| g.run == run && g.len == 0) { Cow::Owned(format!("{text}{BREAK_HYPHENS}")) } else { Cow::Borrowed(text) };
|
||||
let written_text: &str = &written;
|
||||
// The text a line's glyphs stand for: `written`, or its list marker's characters.
|
||||
let text_of = |l: &GlyphLine| l.marker.and_then(|m| layout.marker_of(m)).map_or(written_text, |m| m.text.as_str());
|
||||
// The characters of the list marker being written that each of its glyphs stands for.
|
||||
let mut marker: Option<(usize, Vec<Option<std::ops::Range<usize>>>)> = None;
|
||||
for (gi, g) in layout.glyphs.iter().enumerate().filter(|(_, g)| g.run == run) {
|
||||
let coloured = g.image.is_some() || g.colour.is_some();
|
||||
if coloured && !images {
|
||||
@@ -1815,11 +1825,27 @@ impl Exporter<'_> {
|
||||
(Some(true), false) => continue,
|
||||
_ => {}
|
||||
}
|
||||
// A list marker's glyphs stand for no character of the text either; they are not
|
||||
// line-break hyphens, whatever the marker.
|
||||
let hyphen = if layout.marker_of(gi).is_some() { None } else { break_hyphen(g) };
|
||||
let range = hyphen.clone().map_or(g.byte..g.byte + g.len, |h| text.len() + h.start..text.len() + h.end);
|
||||
let ch = written.get(range.clone()).and_then(|t| t.chars().next());
|
||||
// Of the glyphs with no length, only a list marker's and line-break hyphens are written
|
||||
// as text; the others, such as kashidas, are drawn as paths. A list marker's glyphs are
|
||||
// not line-break hyphens, whatever the marker.
|
||||
let in_marker = (g.len == 0).then(|| layout.marker_of(gi)).flatten();
|
||||
let hyphen = if in_marker.is_some() { None } else { break_hyphen(g) };
|
||||
let (range, ch) = match in_marker {
|
||||
Some(m) => {
|
||||
if marker.as_ref().is_none_or(|(start, _)| *start != m.glyphs.start) {
|
||||
marker = Some((m.glyphs.start, marker_chars(layout, m)));
|
||||
}
|
||||
let range = marker.as_ref().and_then(|(start, chars)| chars.get(gi.checked_sub(*start)?).cloned().flatten());
|
||||
let ch = range.clone().and_then(|r| m.text.get(r)).and_then(|t| t.chars().next());
|
||||
(range.unwrap_or(0..0), ch)
|
||||
}
|
||||
None => {
|
||||
let range = hyphen.clone().map_or(g.byte..g.byte + g.len, |h| text.len() + h.start..text.len() + h.end);
|
||||
let ch = written.get(range.clone()).and_then(|t| t.chars().next());
|
||||
(range, ch)
|
||||
}
|
||||
};
|
||||
let marker_at = in_marker.map(|m| m.glyphs.start);
|
||||
// Spaces are written too (text extraction needs them); control characters and soft
|
||||
// hyphens draw nothing.
|
||||
let blank = g.outline.elements().is_empty() && !coloured;
|
||||
@@ -1852,18 +1878,22 @@ impl Exporter<'_> {
|
||||
let frame = g.xf * Affine::scale(1.0 / sy);
|
||||
// Its pen position along the frame's baseline, when it continues the line (a
|
||||
// line-break hyphen never does).
|
||||
let x = line.as_ref().filter(|l| hyphen.is_none() && l.face == g.font_id && (l.size - size).abs() < 1e-6).and_then(|l| {
|
||||
let [a, b, c, d, e, f] = (l.frame.inverse() * frame).as_coeffs();
|
||||
let same = (a - 1.0).abs() < 1e-6 && b.abs() < 1e-6 && c.abs() < 1e-6 && (d - 1.0).abs() < 1e-6 && f.abs() < 1e-4;
|
||||
same.then_some(e)
|
||||
});
|
||||
let x = line
|
||||
.as_ref()
|
||||
.filter(|l| hyphen.is_none() && l.face == g.font_id && (l.size - size).abs() < 1e-6 && l.marker == marker_at)
|
||||
.and_then(|l| {
|
||||
let [a, b, c, d, e, f] = (l.frame.inverse() * frame).as_coeffs();
|
||||
let same = (a - 1.0).abs() < 1e-6 && b.abs() < 1e-6 && c.abs() < 1e-6 && (d - 1.0).abs() < 1e-6 && f.abs() < 1e-4;
|
||||
same.then_some(e)
|
||||
});
|
||||
let x = match x {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
if let Some(l) = line.take() {
|
||||
self.draw_glyph_line(s, l, &written, fill, layout.bounds);
|
||||
let text = text_of(&l);
|
||||
self.draw_glyph_line(s, l, text, fill, layout.bounds);
|
||||
}
|
||||
line = Some(GlyphLine { face: g.font_id, font, size, frame, glyphs: vec![] });
|
||||
line = Some(GlyphLine { face: g.font_id, font, size, frame, glyphs: vec![], marker: marker_at });
|
||||
0.0
|
||||
}
|
||||
};
|
||||
@@ -1880,7 +1910,8 @@ impl Exporter<'_> {
|
||||
}
|
||||
}
|
||||
if let Some(l) = line {
|
||||
self.draw_glyph_line(s, l, &written, fill, layout.bounds);
|
||||
let text = text_of(&l);
|
||||
self.draw_glyph_line(s, l, text, fill, layout.bounds);
|
||||
}
|
||||
(rest, unwritten)
|
||||
}
|
||||
@@ -2129,6 +2160,35 @@ fn break_hyphen(g: &vectorcraft_text::PositionedGlyph) -> Option<std::ops::Range
|
||||
BREAK_HYPHENS.char_indices().find(|&(_, c)| face.glyph_for(c) == g.gid).map(|(i, c)| i..i + c.len_utf8())
|
||||
}
|
||||
|
||||
/// The characters of list marker `m` of `layout` that each of its glyphs stands for, as bytes of
|
||||
/// the marker's text, in the order of [`ListMarker::glyphs`]: `None` for its tab and for a glyph
|
||||
/// that stands for none of them. A glyph stands for the first character left that the glyph's
|
||||
/// font maps to that glyph in its character map. When the glyphs that no character maps to (an
|
||||
/// OpenType feature such as oldstyle figures replaced them) are as many as the characters left,
|
||||
/// they stand for those characters, in order.
|
||||
fn marker_chars(layout: &TextLayout, m: &ListMarker) -> Vec<Option<std::ops::Range<usize>>> {
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let mut left: Vec<(std::ops::Range<usize>, char)> = m.text.char_indices().map(|(i, c)| (i..i + c.len_utf8(), c)).collect();
|
||||
let mut chars = vec![None; m.glyphs.len()];
|
||||
let mut unmapped = vec![];
|
||||
for (k, gi) in m.glyphs.clone().enumerate() {
|
||||
let Some(g) = layout.glyphs.get(gi).filter(|g| gi != m.tab && g.gid != 0) else { continue };
|
||||
let face = db.face_by_id(g.font_id);
|
||||
match (left.iter().position(|(_, c)| face.as_ref().is_some_and(|f| f.glyph_for(*c) == g.gid)), chars.get_mut(k)) {
|
||||
(Some(at), Some(slot)) => *slot = Some(left.remove(at).0),
|
||||
_ => unmapped.push(k),
|
||||
}
|
||||
}
|
||||
if unmapped.len() == left.len() {
|
||||
for (k, (range, _)) in unmapped.into_iter().zip(left) {
|
||||
if let Some(slot) = chars.get_mut(k) {
|
||||
*slot = Some(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
chars
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -5,8 +5,9 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::{Color, Gradient, GradientPaint, Paint};
|
||||
use vectorcraft_doc::{CharStyle, Document, Effect, Node, NodeKind, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Rect};
|
||||
use vectorcraft_doc::text::{Bullet, NumberStyle, ParaList};
|
||||
use vectorcraft_doc::{CharStyle, Document, Effect, Node, NodeKind, ParaStyle, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_geom::{Affine, BezPath, PathData, Point, Rect, shapes};
|
||||
use vectorcraft_text::{FALLBACK_FAMILY, FontDb};
|
||||
|
||||
use crate::*;
|
||||
@@ -303,6 +304,196 @@ fn real_text_lands_within_half_a_point_of_the_outlines() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Area type 200 points wide holding `text`, its paragraphs in lists `lists`, with an 18 pt
|
||||
/// hanging indent.
|
||||
fn listed(text: &str, lists: &[Option<ParaList>]) -> TextObject {
|
||||
let mut t = TextObject::point(Point::ZERO, text, style(12.0));
|
||||
t.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(20.0, 20.0, 220.0, 280.0)) };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.para.left_indent = 18.0;
|
||||
t.para.first_line_indent = -18.0;
|
||||
let styles = lists.iter().map(|&list| ParaStyle { list, ..t.para.clone() }).collect();
|
||||
t.set_paragraph_styles(styles);
|
||||
t
|
||||
}
|
||||
|
||||
/// The origins (document space) of the glyphs of each list marker of the type objects of `d`, in
|
||||
/// the order they are drawn, the marker's tab left out.
|
||||
fn marker_origins(d: &Document) -> Vec<Vec<Point>> {
|
||||
let mut out = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
let lay = vectorcraft_text::layout(FontDb::global(), t);
|
||||
for m in &lay.markers {
|
||||
out.push(m.glyphs.clone().filter(|&i| i != m.tab).map(|i| t.xf * lay.glyphs[i].origin).collect());
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Each glyph of the type objects of `d` that stands for characters of its text: the first of
|
||||
/// them and the glyph's origin in document space.
|
||||
fn glyph_chars(d: &Document) -> Vec<(char, Point)> {
|
||||
let mut out = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
let text = t.plain_text();
|
||||
for g in vectorcraft_text::layout(FontDb::global(), t).glyphs.iter().filter(|g| g.len > 0) {
|
||||
if let Some(c) = text.get(g.byte..).and_then(|s| s.chars().next()) {
|
||||
out.push((c, t.xf * g.origin));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Does `text` hold `a` before `b`?
|
||||
fn before(text: &str, a: &str, b: &str) -> bool {
|
||||
matches!((text.find(a), text.find(b)), (Some(x), Some(y)) if x < y)
|
||||
}
|
||||
|
||||
/// `back`, the export of `d` reopened, has glyphs of the characters `drawn` (each list marker's,
|
||||
/// in the order the canvas draws them) within `near` points of where `d` draws its markers'
|
||||
/// glyphs.
|
||||
fn assert_markers_in_place(d: &Document, back: &Document, drawn: &[&str], near: f64) {
|
||||
let want = marker_origins(d);
|
||||
assert_eq!(want.len(), drawn.len(), "{drawn:?}");
|
||||
let got = glyph_chars(back);
|
||||
for (origins, chars) in want.iter().zip(drawn) {
|
||||
assert_eq!(origins.len(), chars.chars().count(), "{chars}");
|
||||
for (p, c) in origins.iter().zip(chars.chars()) {
|
||||
assert!(got.iter().any(|(gc, gp)| *gc == c && gp.distance(*p) < near), "{c:?} at {p:?}: {got:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List markers (Paragraph panel › Bullets and Numbered List) are written as text: reopened, the
|
||||
/// export holds each bullet and number as characters, before its paragraph's text and where the
|
||||
/// canvas draws them, and the page looks as it does with outlined type.
|
||||
#[test]
|
||||
fn list_markers_are_written_as_text_where_the_canvas_draws_them() {
|
||||
let number = Some(ParaList::numbered(NumberStyle::Arabic));
|
||||
for (bullet, character) in [(Bullet::Dot, "\u{2022}"), (Bullet::Dash, "\u{2013}"), (Bullet::Circle, "\u{25E6}"), (Bullet::Square, "\u{25AA}")] {
|
||||
let bullet = Some(ParaList::bulleted(bullet));
|
||||
let t = listed("alpha beta gamma delta epsilon zeta eta theta\nbullet\nfirst\nsecond", &[bullet, bullet, number, number]);
|
||||
let d = doc(vec![t]);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
let all = texts(&back).concat();
|
||||
assert_eq!(all.matches(character).count(), 2, "{all}");
|
||||
assert!(before(&all, character, "alpha") && before(&all, "1.", "first") && before(&all, "2.", "second"), "{all}");
|
||||
assert_markers_in_place(&d, &back, &[character, character, "1.", "2."], 0.1);
|
||||
let (real, outlined) = (ink(&import_as(&pdf(&d, false).bytes, TextAs::Outlines)), ink(&import_as(&pdf(&d, true).bytes, TextAs::Outlines)));
|
||||
let off =
|
||||
[real.x0 - outlined.x0, real.y0 - outlined.y0, real.x1 - outlined.x1, real.y1 - outlined.y1].iter().fold(0.0f64, |m, v| m.max(v.abs()));
|
||||
assert!(off < 0.5, "{character}: {real:?} vs {outlined:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A right-to-left numbered paragraph's number is written as its characters where the canvas
|
||||
/// draws them (the period, then 1 and 2, from the left), and the paragraph's own text comes back
|
||||
/// as it does without a list.
|
||||
#[test]
|
||||
fn a_right_to_left_number_is_written_as_text_where_the_canvas_draws_it() {
|
||||
let hebrew = "שלום עולם";
|
||||
let mut plain = listed(hebrew, &[None]);
|
||||
plain.edit_paras(None, |p| p.justify = vectorcraft_doc::Justify::Auto);
|
||||
let mut numbered = plain.clone();
|
||||
numbered.edit_paras(None, |p| p.list = Some(ParaList::numbered(NumberStyle::Arabic)));
|
||||
numbered.list_count = 11;
|
||||
let d = doc(vec![numbered]);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
// The reader tracks a reopened right-to-left line as a whole, and the gap before the number
|
||||
// comes back as a space of the Hebrew font's width, so the number may sit up to a tenth of an
|
||||
// em (1.2 pt) away; its characters keep their spacing to within 0.1 pt.
|
||||
assert_markers_in_place(&d, &back, &[".12"], 1.2);
|
||||
let drawn = marker_origins(&d).concat();
|
||||
let got = glyph_chars(&back);
|
||||
let at = |c: char| got.iter().find(|(gc, _)| *gc == c).map(|(_, p)| p.x);
|
||||
for (k, (a, b)) in [('.', '1'), ('1', '2')].into_iter().enumerate() {
|
||||
let (Some(pa), Some(pb), Some(da), Some(db)) = (at(a), at(b), drawn.get(k), drawn.get(k + 1)) else {
|
||||
panic!("{a:?} and {b:?} in {got:?}");
|
||||
};
|
||||
assert!(((pb - pa) - (db.x - da.x)).abs() < 0.1, "{a:?} to {b:?}: {} against {}", pb - pa, db.x - da.x);
|
||||
}
|
||||
let without = texts(&import_as(&pdf(&doc(vec![plain]), false).bytes, TextAs::Text)).concat();
|
||||
let all = texts(&back).concat();
|
||||
assert!(all.contains(without.trim()), "{all:?} holds {without:?}");
|
||||
}
|
||||
|
||||
/// Type on a path writes its bullet as text where the canvas draws it along the path.
|
||||
#[test]
|
||||
fn a_bullet_on_a_path_is_written_as_text_where_the_canvas_draws_it() {
|
||||
let mut t = TextObject::point(Point::ZERO, "along the arc", style(18.0));
|
||||
t.kind = TextKind::OnPath { path: arc(), start: 0.0, end: None };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.para.list = Some(ParaList::bulleted(Bullet::Dot));
|
||||
let d = doc(vec![t]);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
assert!(texts(&back).concat().contains('\u{2022}'), "{:?}", texts(&back));
|
||||
assert_markers_in_place(&d, &back, &["\u{2022}"], 0.3);
|
||||
}
|
||||
|
||||
/// Threaded frames write each marker their layouts draw as text, the number of the paragraph the
|
||||
/// first frame splits included, in each frame where the canvas draws it.
|
||||
#[test]
|
||||
fn numbers_in_threaded_frames_are_written_as_text_in_each_frame() {
|
||||
let numbered =
|
||||
ParaStyle { list: Some(ParaList::numbered(NumberStyle::Arabic)), left_indent: 18.0, first_line_indent: -18.0, ..Default::default() };
|
||||
let frame = |r: Rect| {
|
||||
let mut t = TextObject::point(Point::ZERO, "", style(12.0));
|
||||
t.kind = TextKind::Area { frame: shapes::rectangle(r) };
|
||||
t.xf = Affine::IDENTITY;
|
||||
t
|
||||
};
|
||||
let (mut a, mut b) = (frame(Rect::new(20.0, 20.0, 180.0, 80.0)), frame(Rect::new(200.0, 20.0, 360.0, 280.0)));
|
||||
let text = format!("One\nTwo\n{}\nFour", "The quick brown fox jumps over the lazy dog. ".repeat(4));
|
||||
let story = vec![TextRun { text, style: style(12.0), inline: None }];
|
||||
let parts = vectorcraft_text::thread::distribute(FontDb::global(), &[&a, &b], &story, &vec![numbered; 4]);
|
||||
for (f, part) in [&mut a, &mut b].into_iter().zip(&parts) {
|
||||
f.runs = part.runs.clone();
|
||||
f.set_paragraph_styles(part.paras.clone());
|
||||
f.list_count = part.list_count;
|
||||
}
|
||||
let first = a.plain_text();
|
||||
assert!(first.contains("quick") && !first.ends_with('\n'), "the first frame splits the third paragraph: {first:?}");
|
||||
let d = doc(vec![a, b]);
|
||||
let mut drawn: Vec<String> = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
drawn.extend(vectorcraft_text::layout(FontDb::global(), t).markers.into_iter().map(|m| m.text));
|
||||
}
|
||||
});
|
||||
assert!(drawn.contains(&"4.".to_string()), "{drawn:?}");
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
assert_markers_in_place(&d, &back, &drawn.iter().map(String::as_str).collect::<Vec<_>>(), 0.1);
|
||||
}
|
||||
|
||||
/// A number set in oldstyle figures (the first character's OpenType features) is written as its
|
||||
/// digits, though the character map gives the digits other glyphs. The reopened type has no
|
||||
/// OpenType features and sets lining figures, so only where each number starts is compared.
|
||||
#[test]
|
||||
fn a_number_in_oldstyle_figures_is_written_as_its_digits() {
|
||||
let number = Some(ParaList::numbered(NumberStyle::Arabic));
|
||||
let mut t = listed("first\nsecond", &[number, number]);
|
||||
for r in &mut t.runs {
|
||||
r.style.features = vec!["onum".into()];
|
||||
}
|
||||
let lay = vectorcraft_text::layout(FontDb::global(), &t);
|
||||
let one = &lay.glyphs[lay.markers[0].glyphs.start];
|
||||
let face = FontDb::global().face_by_id(one.font_id).unwrap();
|
||||
assert_ne!(face.glyph_for('1'), one.gid, "an oldstyle 1, not the character map's");
|
||||
let d = doc(vec![t]);
|
||||
let back = import_as(&pdf(&d, false).bytes, TextAs::Text);
|
||||
let all = texts(&back).concat();
|
||||
assert!(before(&all, "1.", "first") && before(&all, "2.", "second"), "{all:?}");
|
||||
let got = glyph_chars(&back);
|
||||
for (origins, digit) in marker_origins(&d).iter().zip(['1', '2']) {
|
||||
assert!(got.iter().any(|(c, p)| *c == digit && p.distance(origins[0]) < 0.1), "{digit} at {:?}: {got:?}", origins[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/// A run shifted two billion points off its baseline is left out, as real text or outlines, and
|
||||
/// the rest of the type is written. On a page 50 points high, the page's y flip puts the shifted
|
||||
/// run's baseline at -2,147,483,648, which the PDF writer panics on in builds with overflow checks.
|
||||
|
||||
+9
-4
@@ -4531,10 +4531,15 @@ after Remove Threading or Release Selection, in a frame converted to point type,
|
||||
duplicated, copied in the Layers panel, or taken out of a symbol or a placed file). A symbol made of a thread keeps the
|
||||
numbers in the symbol's art.
|
||||
|
||||
The canvas, EPS, PDF and Type › Create Outlines draw the marker; in PDF it is a path, not text. Live SVG text writes the
|
||||
marker's characters before the paragraph's text, except type on a path with a list, which it writes as outlines with a
|
||||
warning. DXF writes the marker as a TEXT entity of its own, its characters in the order they are drawn. Legacy `.ai`
|
||||
writes the paragraph's text without the marker and warns that bulleted and numbered lists are left out.
|
||||
The canvas, EPS, PDF and Type › Create Outlines draw the marker. PDF writes the marker as text of its own, where the
|
||||
canvas draws it, and a PDF viewer selects, copies and finds its characters. Each of its glyphs stands for the marker
|
||||
character that the glyph's font maps to that glyph in its character map. Glyphs that an OpenType feature such as
|
||||
oldstyle figures replaced stand for the characters left over, in order, when there are as many of those glyphs as
|
||||
characters left over. A marker glyph that stands for no character, such as the missing-glyph box when no font has a
|
||||
glyph for it, is drawn as a path. Live SVG text writes the marker's characters before the paragraph's text, except
|
||||
type on a path with a list, which it writes as outlines with a warning. DXF writes the marker as a TEXT entity of its
|
||||
own, its characters in the order they are drawn. Legacy `.ai` writes the paragraph's text without the marker and warns
|
||||
that bulleted and numbered lists are left out.
|
||||
|
||||
Type › Bullets and Numbering › Apply Bullets and the canvas context menu's Bullets and Numbering › Apply Bullets run
|
||||
`type.applyBullets {bullet?}` on the selected type's paragraphs, or on those the Type tool's selection touches. Without
|
||||
|
||||
@@ -792,6 +792,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Paragraph panel's Numbered List button, Type › Bullets and Numbering › Apply Numbers and the context menu's Apply Numbers (`text.setFormat {list, number}`, `type.applyNumbers`) make paragraphs a numbered list with the documented presets (1, A, a, I, i, 01, 001, and None, which turns the list off; [Bullets and numbering](https://web.archive.org/web/20250807202418/https://helpx.adobe.com/illustrator/using/bullets-numbering.html), [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html)): each paragraph starts with its number and a period, counted through the list paragraphs of the story and its threaded frames, with the text and indents as in a bulleted list; letters go on from Z to AA, and roman numerals go on from III to IV, an empty paragraph is not numbered, and a paragraph in no list starts the count again at 1.
|
||||
Type › Bullets and Numbering › Convert to Text (`type.convertListToText`, `text.convertListToText`) makes the bullets and numbers of the list paragraphs editable characters of their text ([Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html), [December 2022 release notes](https://web.archive.org/web/20260203094741/https://helpx.adobe.com/illustrator/using/whats-new/2023-1.html)): each marker becomes its characters and a tab at the start of its paragraph, in the marker's font, size and color, the list is turned off, and a left tab stop at the left indent keeps the text after the tab where it started; threaded type keeps the numbers its story counted.
|
||||
The Type tool starts a list from the characters [Create bulleted or numbered lists](https://web.archive.org/web/20260417034638/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-bulleted-or-numbered-lists.html) names (`text.startList`): a space typed on its own after `1.`, `1)`, `A.` or `A)` at the start of a paragraph in no list makes the paragraph a numbered list in 1, 2, 3 or A, B, C, and a space typed after `*`, `#`, `>` or `+` makes it a bulleted list with the filled dot. The typed characters are removed, and one Undo puts them back as text with no list. The same characters later in a paragraph, and pasted list characters, start nothing.
|
||||
PDF export writes bullets and numbers as text, in the glyphs and at the places the canvas draws them, in area type, type on a path, right-to-left paragraphs and threaded frames, and a PDF viewer selects, copies and finds them; a marker glyph that stands for no character is drawn as a path.
|
||||
The Character panel menu's Character Direction (with the Indic options; `text.setFormat {charDirection}`, `text.setRangeStyle {charDirection}`) sets the selected characters left to right or right to left inside their paragraph, as a Unicode bidi embedding: a date set Left to Right keeps its order in right-to-left text, and a word set Right to Left keeps the punctuation after it on its left. Default (the default) leaves the characters to the bidi algorithm; character styles and the native file keep the setting.
|
||||
In vertical type, the Tabs panel names its alignment buttons Top-Justified Tab, Center-Justified Tab, Bottom-Justified Tab and Decimal-Justified Tab, in their tool tips and for screen readers, and labels the position field Y. In horizontal type the buttons keep the names Left-Justified Tab and Right-Justified Tab, and the field keeps the label X.
|
||||
The Tabs panel's ruler is numbered in the General unit with the canvas rulers' label steps and a tick every tenth of a step. With the panel menu's Snap to Unit on, or Shift held, a tab stop placed by a click on the ruler or dragged on it lands on the nearest tick (`text.tabs.set`).
|
||||
|
||||
Reference in New Issue
Block a user