mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
Lay out vertical (Identity-V) text as columns in the edit layer
Fixes #3216 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
dab3149110
commit
20cfb67dd1
@@ -4975,3 +4975,89 @@ fn flattening_comments_keeps_annotations_that_are_not_comments() {
|
||||
let without_widget: Vec<&str> = all.iter().copied().filter(|s| *s != "Widget").collect();
|
||||
assert_eq!(left(&reopen(&doc)), without_widget, "flattening fields touches no annotation that isn't a widget");
|
||||
}
|
||||
|
||||
/// A page of three vertical (`Identity-V`) columns at 20 pt: `ABCDEF` at x = 500, the same
|
||||
/// text 30 pt to its left, and `ABCD` far away. Synthetic font: no font program is needed.
|
||||
fn vertical_columns_page() -> Document {
|
||||
let mut doc = text_page(
|
||||
"BT /V1 20 Tf 500 720 Td <004100420043004400450046> Tj ET\nBT /V1 20 Tf 470 720 Td <004100420043004400450046> Tj ET\nBT /V1 20 Tf 300 720 Td <0041004200430044> Tj ET",
|
||||
);
|
||||
let mut font = Dict::from_iter([
|
||||
(b"Type".to_vec(), Object::name("Font")),
|
||||
(b"Subtype".to_vec(), Object::name("Type0")),
|
||||
(b"BaseFont".to_vec(), Object::name("SyntheticCID")),
|
||||
(b"Encoding".to_vec(), Object::name("Identity-V")),
|
||||
]);
|
||||
let unicode = doc.add(Object::Stream(Stream::flate(
|
||||
Dict::new(),
|
||||
b"1 begincodespacerange <0000> <ffff> endcodespacerange 6 beginbfchar <0041> <0041> <0042> <0042> <0043> <0043> <0044> <0044> <0045> <0045> <0046> <0046> endbfchar",
|
||||
)));
|
||||
font.set(b"ToUnicode".to_vec(), Object::Ref(unicode));
|
||||
font.set(
|
||||
b"DescendantFonts".to_vec(),
|
||||
Object::Array(vec![Object::Dict(Dict::from_iter([
|
||||
(b"Subtype".to_vec(), Object::name("CIDFontType2")),
|
||||
(b"DW".to_vec(), Object::Int(1000)),
|
||||
]))]),
|
||||
);
|
||||
let p = pdfcraft_model::pages(&doc)[0].obj;
|
||||
let fonts = Dict::from_iter([(b"V1".to_vec(), Object::Dict(font))]);
|
||||
let resources = Dict::from_iter([(b"Font".to_vec(), Object::Dict(fonts))]);
|
||||
doc.update_dict(p, |d| d.set(b"Resources".to_vec(), Object::Dict(resources))).unwrap();
|
||||
doc
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_text_lines_are_columns_and_editing_one_keeps_its_neighbour() {
|
||||
let doc = vertical_columns_page();
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
let first = lines.iter().find(|l| l.baseline()[0] == 500.0).unwrap();
|
||||
let r = first.rect;
|
||||
// Six glyphs, each 20 pt wide and advancing 20 pt down, centred on x = 500.
|
||||
assert!((r[0] - 490.0).abs() < 0.5 && (r[2] - 510.0).abs() < 0.5, "{r:?}");
|
||||
assert!((r[1] - 600.0).abs() < 0.5 && (r[3] - 720.0).abs() < 0.5, "{r:?}");
|
||||
for edit in [0, 1] {
|
||||
let mut doc = vertical_columns_page();
|
||||
let lines = text::text_lines(&doc, 0).unwrap();
|
||||
if edit == 0 {
|
||||
let line = lines.iter().position(|l| l.baseline()[0] == 500.0).unwrap();
|
||||
text::replace_line(&mut doc, 0, line, "ABCDEA").unwrap();
|
||||
} else {
|
||||
let blocks = text::text_blocks(&doc, 0).unwrap();
|
||||
let block = blocks.iter().position(|b| b.lines.iter().any(|&i| lines[i].baseline()[0] == 500.0)).unwrap();
|
||||
assert_eq!(blocks[block].lines.len(), 1, "{blocks:?}");
|
||||
text::replace_block(&mut doc, 0, block, "ABCDEA").unwrap();
|
||||
}
|
||||
let after = text::text_lines(&doc, 0).unwrap();
|
||||
let texts: Vec<(f64, &str)> = after.iter().map(|l| (l.baseline()[0], l.text.as_str())).collect();
|
||||
assert!(texts.contains(&(500.0, "ABCDEA")), "{texts:?}");
|
||||
assert!(texts.contains(&(470.0, "ABCDEF")), "{texts:?}");
|
||||
assert!(texts.contains(&(300.0, "ABCD")), "{texts:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_whose_cmap_writing_mode_cannot_be_read_is_not_edited() {
|
||||
let mut doc = vertical_columns_page();
|
||||
// An embedded CMap that uses itself: which way its glyphs advance can't be known.
|
||||
let cmap = doc.add(Object::Null);
|
||||
doc.set(
|
||||
cmap,
|
||||
Object::Stream(Stream::flate(
|
||||
Dict::from_iter([(b"UseCMap".to_vec(), Object::Ref(cmap))]),
|
||||
b"1 begincodespacerange <0000> <ffff> endcodespacerange",
|
||||
)),
|
||||
);
|
||||
let p = pdfcraft_model::pages(&doc)[0].obj;
|
||||
doc.update_dict(p, |d| {
|
||||
if let Some(Object::Dict(res)) = d.get_mut(b"Resources")
|
||||
&& let Some(Object::Dict(fonts)) = res.get_mut(b"Font")
|
||||
&& let Some(Object::Dict(font)) = fonts.get_mut(b"V1")
|
||||
{
|
||||
font.set(b"Encoding".to_vec(), Object::Ref(cmap));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let error = text::replace_line(&mut doc, 0, 0, "ABCDEA").unwrap_err();
|
||||
assert!(error.to_string().contains("cyclic"), "{error}");
|
||||
}
|
||||
|
||||
+108
-29
@@ -519,6 +519,9 @@ struct Ts {
|
||||
leading: f64,
|
||||
rise: f64,
|
||||
render_mode: i64,
|
||||
/// The font's vertical advance per unit of font size (`/DW2`'s w1, normally −1) when its
|
||||
/// CMap writes vertically (`WMode 1`, §9.7.4.3): each glyph then moves the pen down, not right.
|
||||
vertical: Option<f64>,
|
||||
}
|
||||
|
||||
fn content_streams(doc: &Document, page: &Dict) -> Vec<(Object, Vec<u8>)> {
|
||||
@@ -684,12 +687,13 @@ struct Pen {
|
||||
across: f64,
|
||||
end: f64,
|
||||
size: f64,
|
||||
vertical: bool,
|
||||
}
|
||||
|
||||
impl Pen {
|
||||
fn after(s: &Shown) -> Self {
|
||||
let (dir, _, end, across) = s.span();
|
||||
Pen { bt: s.bt, dir, across, end, size: s.size.max(1.0) }
|
||||
Pen { bt: s.bt, dir, across, end, size: s.size.max(1.0), vertical: s.state.vertical.is_some() }
|
||||
}
|
||||
|
||||
/// Whether `s` could continue the same line on geometry alone: on the same baseline, starting
|
||||
@@ -698,6 +702,8 @@ impl Pen {
|
||||
let (dir, start, _, across) = s.span();
|
||||
let gap = start - self.end;
|
||||
let same_dir = (dir.0 - self.dir.0).abs() < 1e-3 && (dir.1 - self.dir.1).abs() < 1e-3;
|
||||
// The geometry above is along a horizontal baseline: vertical runs never continue one.
|
||||
let same_dir = same_dir && !self.vertical && s.state.vertical.is_none();
|
||||
(same_dir && (across - self.across).abs() < self.size * 0.3 && gap > -self.size && gap < self.size * reach, gap)
|
||||
}
|
||||
}
|
||||
@@ -725,11 +731,16 @@ impl Carry {
|
||||
leading: 0.0,
|
||||
rise: 0.0,
|
||||
render_mode: 0,
|
||||
vertical: None,
|
||||
};
|
||||
Carry { ts, stack: Vec::new(), marked: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A font resource's metrics, fallback face and vertical advance (see [`Ts`]), read once per page.
|
||||
/// The advance is an error message when the font's writing mode can't be read.
|
||||
type CachedFont = (Rc<Metrics>, Option<String>, Result<Option<f64>, String>);
|
||||
|
||||
/// A `Do` met while interpreting: its operator index, XObject name and the state there.
|
||||
type Invocation = (usize, Vec<u8>, Ts);
|
||||
|
||||
@@ -737,12 +748,12 @@ fn interpret(
|
||||
doc: &Document,
|
||||
ops: &[Op],
|
||||
fonts_res: &Dict,
|
||||
cache: &mut HashMap<Vec<u8>, (Rc<Metrics>, Option<String>)>,
|
||||
cache: &mut HashMap<Vec<u8>, CachedFont>,
|
||||
carry: &mut Carry,
|
||||
mut invoked: Option<&mut Vec<Invocation>>,
|
||||
// `Some(properties)`: read `/ActualText`, looking named property lists up in `properties`.
|
||||
logical_text: Option<&Dict>,
|
||||
) -> Vec<Shown> {
|
||||
) -> Result<Vec<Shown>, EditError> {
|
||||
let mut out: Vec<Shown> = Vec::new();
|
||||
let mut ts = carry.ts.clone();
|
||||
let mut at_bt = (0usize, ts.clone());
|
||||
@@ -828,14 +839,17 @@ fn interpret(
|
||||
b"Tf" => {
|
||||
ts.size = op.num(1).unwrap_or(ts.size);
|
||||
if let Some(name) = op.name(0) {
|
||||
let (m, face) = cache.entry(name.to_vec()).or_insert_with(|| {
|
||||
let (m, face, vertical) = cache.entry(name.to_vec()).or_insert_with(|| {
|
||||
let dict = fonts_res.get(name).and_then(|f| doc.resolve(f).as_dict().cloned());
|
||||
let metrics = dict.as_ref().map_or_else(Metrics::fallback, |d| Metrics::from_dict(doc, d));
|
||||
let vertical = dict.as_ref().map_or(Ok(None), |d| vertical_advance(doc, d).map_err(|e| e.to_string()));
|
||||
let face = dict.filter(is_type3_fallback).map(|_| metrics.base_font.clone());
|
||||
(Rc::new(metrics), face)
|
||||
(Rc::new(metrics), face, vertical)
|
||||
});
|
||||
ts.font = Some((name.to_vec(), m.clone()));
|
||||
ts.face = face.clone();
|
||||
// Which way the glyphs advance is unknown: refuse rather than guess (§9.7.4.3).
|
||||
ts.vertical = vertical.clone().map_err(EditError::Invalid)?;
|
||||
}
|
||||
}
|
||||
b"Tc" => ts.char_spacing = op.num(0).unwrap_or(0.0),
|
||||
@@ -888,6 +902,10 @@ fn interpret(
|
||||
let mut text = String::new();
|
||||
let mut decodable = true;
|
||||
let mut x_text = 0.0;
|
||||
// Vertical text (§9.4.4): the pen moves along y instead, and each glyph is centred
|
||||
// on it horizontally; `half` is half the widest glyph.
|
||||
let vertical = ts.vertical;
|
||||
let (mut y_text, mut half) = (0.0, 0.0_f64);
|
||||
// Where each word's glyphs start and end along the run (text space), and whether
|
||||
// the next glyph continues the last word.
|
||||
let mut words: Vec<(f64, f64)> = Vec::new();
|
||||
@@ -905,6 +923,21 @@ fn interpret(
|
||||
Some(t) => text.push_str(&t),
|
||||
None => decodable = false,
|
||||
}
|
||||
let spacing = ts.char_spacing + if m.is_space(code, len) { ts.word_spacing } else { 0.0 };
|
||||
if let Some(w1) = vertical {
|
||||
half = half.max((m.width(code) * ts.size * ts.scale).abs() / 2.0);
|
||||
if !blank {
|
||||
let ink_end = y_text + w1 * ts.size;
|
||||
let (lo, hi) = (y_text.min(ink_end), y_text.max(ink_end));
|
||||
match words.last_mut() {
|
||||
Some(word) if in_word => *word = (word.0.min(lo), word.1.max(hi)),
|
||||
_ => words.push((lo, hi)),
|
||||
}
|
||||
}
|
||||
in_word = !blank;
|
||||
y_text += w1 * ts.size + spacing;
|
||||
continue;
|
||||
}
|
||||
if !blank {
|
||||
let ink_end = x_text + m.width(code) * ts.size * ts.scale;
|
||||
// A negative font size or `Tz` runs the glyphs backwards:
|
||||
@@ -922,6 +955,18 @@ fn interpret(
|
||||
}
|
||||
other => {
|
||||
if let Some(n) = other.as_f64() {
|
||||
if vertical.is_some() {
|
||||
// Down by n/1000 em, not scaled by `Tz`.
|
||||
let dy = -n / 1000.0 * ts.size;
|
||||
if -dy > ts.size * 0.2 && !text.ends_with(' ') {
|
||||
text.push(' ');
|
||||
}
|
||||
if dy.abs() > ts.size.abs() * 0.2 {
|
||||
in_word = false;
|
||||
}
|
||||
y_text += dy;
|
||||
continue;
|
||||
}
|
||||
let dx = -n / 1000.0 * ts.size * ts.scale;
|
||||
// A large gap inside TJ reads as a space.
|
||||
if dx > ts.size * 0.2 && !text.ends_with(' ') {
|
||||
@@ -937,15 +982,22 @@ fn interpret(
|
||||
}
|
||||
}
|
||||
}
|
||||
tm = Matrix([1.0, 0.0, 0.0, 1.0, x_text, 0.0]).then(&tm);
|
||||
tm = Matrix([1.0, 0.0, 0.0, 1.0, x_text, y_text]).then(&tm);
|
||||
let end = tm.then(&ts.ctm).apply(0.0, ts.rise);
|
||||
// The box: from descent to ascent along the run.
|
||||
let corners = [
|
||||
trm0.apply(0.0, ts.rise + m.descent * ts.size),
|
||||
trm0.apply(x_text, ts.rise + m.descent * ts.size),
|
||||
trm0.apply(0.0, ts.rise + m.ascent * ts.size),
|
||||
trm0.apply(x_text, ts.rise + m.ascent * ts.size),
|
||||
];
|
||||
// The box from `a` to `b` along the run: from descent to ascent across it, or for
|
||||
// vertical text, the widest glyph's width centred on the pen.
|
||||
let span = |a: f64, b: f64| match vertical {
|
||||
Some(_) => {
|
||||
[trm0.apply(-half, ts.rise + a), trm0.apply(half, ts.rise + a), trm0.apply(-half, ts.rise + b), trm0.apply(half, ts.rise + b)]
|
||||
}
|
||||
None => [
|
||||
trm0.apply(a, ts.rise + m.descent * ts.size),
|
||||
trm0.apply(b, ts.rise + m.descent * ts.size),
|
||||
trm0.apply(a, ts.rise + m.ascent * ts.size),
|
||||
trm0.apply(b, ts.rise + m.ascent * ts.size),
|
||||
],
|
||||
};
|
||||
let corners = span(0.0, if vertical.is_some() { y_text } else { x_text });
|
||||
let rect = corners
|
||||
.iter()
|
||||
.fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |b, p| [b[0].min(p.0), b[1].min(p.1), b[2].max(p.0), b[3].max(p.1)]);
|
||||
@@ -959,8 +1011,7 @@ fn interpret(
|
||||
let ink = words
|
||||
.iter()
|
||||
.map(|&(a, b)| {
|
||||
let (y0, y1) = (ts.rise + m.descent * ts.size, ts.rise + m.ascent * ts.size);
|
||||
[trm0.apply(a, y0), trm0.apply(b, y0), trm0.apply(a, y1), trm0.apply(b, y1)]
|
||||
span(a, b)
|
||||
.iter()
|
||||
.fold([f64::MAX, f64::MAX, f64::MIN, f64::MIN], |r, p| [r[0].min(p.0), r[1].min(p.1), r[2].max(p.0), r[3].max(p.1)])
|
||||
})
|
||||
@@ -975,8 +1026,17 @@ fn interpret(
|
||||
op: i,
|
||||
extra_ops: Vec::new(),
|
||||
glyph_count,
|
||||
advance: x_text,
|
||||
tm: Matrix([tm.0[0], tm.0[1], tm.0[2], tm.0[3], tm.0[4] - x_text * tm.0[0], tm.0[5] - x_text * tm.0[1]]),
|
||||
// Vertical text: the TJ adjustment that undoes the run is n = -1000 y / size; the
|
||||
// preview divides by size × `Tz`, so `Tz` is put back here.
|
||||
advance: if vertical.is_some() { y_text * ts.scale } else { x_text },
|
||||
tm: Matrix([
|
||||
tm.0[0],
|
||||
tm.0[1],
|
||||
tm.0[2],
|
||||
tm.0[3],
|
||||
tm.0[4] - x_text * tm.0[0] - y_text * tm.0[2],
|
||||
tm.0[5] - x_text * tm.0[1] - y_text * tm.0[3],
|
||||
]),
|
||||
state: ts.clone(),
|
||||
tlm,
|
||||
bt_op: at_bt.0,
|
||||
@@ -1009,7 +1069,7 @@ fn interpret(
|
||||
carry.ts = ts;
|
||||
carry.stack = stack;
|
||||
carry.marked = marked;
|
||||
out
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The lines of text on a page (0-based), in content order.
|
||||
@@ -1025,7 +1085,7 @@ pub fn text_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditErro
|
||||
let pieces = Pieces::join(&content_streams(doc, &p.dict).into_iter().map(|(_, d)| d).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
let properties = res.get(b"Properties").and_then(|o| doc.dict(o)).unwrap_or_default();
|
||||
let shown = interpret(doc, &ops, &fonts_res, &mut cache, &mut carry, None, Some(&properties));
|
||||
let shown = interpret(doc, &ops, &fonts_res, &mut cache, &mut carry, None, Some(&properties))?;
|
||||
let mut last: Option<(usize, Pen)> = None; // the stream piece and where its last run ended
|
||||
for mut s in shown {
|
||||
// A single RTL glyph's ActualText (e.g. lam-alef) is an inline ligature, not a
|
||||
@@ -1264,18 +1324,18 @@ fn reading_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditError
|
||||
visits: usize,
|
||||
}
|
||||
/// `stream` gives the stream number of the operator at an index of `ops`.
|
||||
fn walk(w: &mut Walk, ops: &[Op], resources: &Dict, carry: &mut Carry, stream: &dyn Fn(usize) -> usize) {
|
||||
fn walk(w: &mut Walk, ops: &[Op], resources: &Dict, carry: &mut Carry, stream: &dyn Fn(usize) -> usize) -> Result<(), EditError> {
|
||||
let fonts_res = resources.get(b"Font").map(|f| w.doc.resolve(f)).and_then(|f| f.as_dict().cloned()).unwrap_or_default();
|
||||
let mut cache = HashMap::new();
|
||||
let mut invoked = Vec::new();
|
||||
let properties = resources.get(b"Properties").and_then(|o| w.doc.dict(o)).unwrap_or_default();
|
||||
let shown = interpret(w.doc, ops, &fonts_res, &mut cache, carry, Some(&mut invoked), Some(&properties));
|
||||
let shown = interpret(w.doc, ops, &fonts_res, &mut cache, carry, Some(&mut invoked), Some(&properties))?;
|
||||
let mut last = None;
|
||||
let mut last_stream = None;
|
||||
let mut forms = invoked.into_iter().peekable();
|
||||
for s in shown {
|
||||
while let Some((_, name, ts)) = forms.next_if(|f| f.0 < s.op) {
|
||||
enter(w, resources, &name, ts);
|
||||
enter(w, resources, &name, ts)?;
|
||||
// Text after a form starts a new line.
|
||||
last = None;
|
||||
}
|
||||
@@ -1287,11 +1347,12 @@ fn reading_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditError
|
||||
add_shown(&mut w.lines, &mut last, s, si);
|
||||
}
|
||||
for (_, name, ts) in forms {
|
||||
enter(w, resources, &name, ts);
|
||||
enter(w, resources, &name, ts)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn enter(w: &mut Walk, resources: &Dict, name: &[u8], ts: Ts) {
|
||||
let Some(form) = form_call(w.doc, resources, name, ts.ctm, &w.path, &mut w.visits) else { return };
|
||||
fn enter(w: &mut Walk, resources: &Dict, name: &[u8], ts: Ts) -> Result<(), EditError> {
|
||||
let Some(form) = form_call(w.doc, resources, name, ts.ctm, &w.path, &mut w.visits) else { return Ok(()) };
|
||||
let ops = parse(&form.data).ops;
|
||||
// The form starts with the graphics state at its `Do`, its own matrix applied.
|
||||
let mut carry = Carry { ts: Ts { ctm: form.ctm, ..ts }, stack: Vec::new(), marked: Vec::new() };
|
||||
@@ -1300,10 +1361,11 @@ fn reading_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditError
|
||||
if let Some(r) = form.obj {
|
||||
w.path.push(r);
|
||||
}
|
||||
walk(w, &ops, &form.resources, &mut carry, &|_| stream);
|
||||
walk(w, &ops, &form.resources, &mut carry, &|_| stream)?;
|
||||
if form.obj.is_some() {
|
||||
w.path.pop();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let p = page_dict(doc, page)?;
|
||||
let res = p.dict.get(b"Resources").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
@@ -1317,7 +1379,7 @@ fn reading_lines(doc: &Document, page: usize) -> Result<Vec<TextLine>, EditError
|
||||
let pieces = Pieces::join(&streams.into_iter().map(|(_, data)| data).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
let stream_of = |i: usize| ops.get(i).map_or(0, |op| pieces.pieces_of(op).0);
|
||||
walk(&mut w, &ops, &res, &mut carry, &stream_of);
|
||||
walk(&mut w, &ops, &res, &mut carry, &stream_of)?;
|
||||
let mut lines = w.lines;
|
||||
// A visual-order Indic font draws pre-base vowel signs before their consonant.
|
||||
for l in lines.iter_mut().filter(|l| l.visual_order && !l.actual_text) {
|
||||
@@ -1907,7 +1969,7 @@ pub fn clear_block(doc: &mut Document, page: usize, block: usize) -> Result<(),
|
||||
let streams = content_streams(doc, &p.dict);
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None)?;
|
||||
let mut patches = HashMap::new();
|
||||
for s in shown.into_iter().filter(|s| drops.contains(&s.op)) {
|
||||
if s.state.render_mode >= 4 {
|
||||
@@ -2492,6 +2554,23 @@ pub(crate) fn selectable_blocks(
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// The vertical advance per unit of font size of a font that writes vertically: `/DW2`'s w1
|
||||
/// (default −1000, §9.7.4.3), or `None` for horizontal text. Per-glyph `/W2` entries are not
|
||||
/// read. An encoding whose writing mode can't be read is an error, as in [`vertical_encoding`].
|
||||
fn vertical_advance(doc: &Document, font: &Dict) -> Result<Option<f64>, EditError> {
|
||||
if !vertical_encoding(doc, font)? {
|
||||
return Ok(None);
|
||||
}
|
||||
let descendant = font.get(b"DescendantFonts").and_then(|d| doc.resolve(d).as_array().and_then(|a| a.first().cloned()));
|
||||
let w1 = descendant
|
||||
.and_then(|d| doc.resolve(&d).as_dict().and_then(|d| d.get(b"DW2").cloned()))
|
||||
.and_then(|a| doc.resolve(&a).as_array().and_then(|a| a.get(1).cloned()))
|
||||
.and_then(|w| doc.resolve(&w).as_f64())
|
||||
.filter(|w| w.is_finite())
|
||||
.unwrap_or(-1000.0);
|
||||
Ok(Some(w1 / 1000.0))
|
||||
}
|
||||
|
||||
/// Detect vertical CMaps before moving horizontal text. A broken/cyclic embedded CMap is
|
||||
/// refused rather than guessing which direction TJ advances its text matrix.
|
||||
fn vertical_encoding(doc: &Document, font: &Dict) -> Result<bool, EditError> {
|
||||
@@ -2564,7 +2643,7 @@ pub(crate) fn translate_blocks(doc: &mut Document, page: usize, indexes: &[usize
|
||||
let fonts = res.get(b"Font").map(|r| doc.resolve(r)).and_then(|r| r.as_dict().cloned()).unwrap_or_default();
|
||||
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
|
||||
let ops = pieces.parse().ops;
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None);
|
||||
let shown = interpret(doc, &ops, &fonts, &mut HashMap::new(), &mut Carry::new(), None, None)?;
|
||||
let mut patches = HashMap::new();
|
||||
// Keep full precision: a tiny rounded text-space change can be magnified by a CTM,
|
||||
// and restoring rise/spacing must not alter following unselected text.
|
||||
|
||||
Reference in New Issue
Block a user