mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-11 16:36:13 -06:00
Refactor edit for strict Clippy checks
This commit is contained in:
@@ -2,7 +2,10 @@ use super::layout::{
|
||||
Piece, arabic_error, hebrew_error, is_arabic, is_hebrew, is_thai, latin_width, lines, norm, shaped_layout, text_rect, thai_error, wrapped,
|
||||
};
|
||||
use super::types::{AddedText, Align, Family};
|
||||
use super::*;
|
||||
use super::{
|
||||
Content, Dict, Document, EditError, GlyphError, GlyphOutline, HashMap, HashSet, ObjRef, Object, PdfString, ShapedCluster, ShapedScript, Stream,
|
||||
literal, matrix_number, n, shaped_glyph, win_ansi,
|
||||
};
|
||||
|
||||
fn font_name(base: &str) -> String {
|
||||
format!("PCF{}", base.replace('-', ""))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::types::{AddedImage, AddedText};
|
||||
use super::*;
|
||||
use super::{
|
||||
Content, Document, EditError, GlyphError, Level, ObjRef, Object, ParagraphBidiInfo, ShapedCluster, ShapedScript, shape_arabic, shape_hebrew,
|
||||
shape_thai,
|
||||
};
|
||||
|
||||
pub(super) fn nums(doc: &Document, o: Option<&Object>) -> Vec<f64> {
|
||||
o.map(|o| doc.resolve(o)).and_then(|a| a.as_array().map(|a| a.iter().filter_map(Object::as_f64).collect())).unwrap_or_default()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::draw::{draw, params};
|
||||
use super::layout::{first_undrawable, is_thai, nums, shaped_arabic, thai_error};
|
||||
use super::types::{Added, AddedImage, AddedText, Align, CHAR_SPACING_LIMIT, Family, MarkerScan, TAG};
|
||||
use super::*;
|
||||
use super::{Content, Dict, Document, EditError, GlyphError, MarkKind, ObjRef, Object, Stream, check, contents, page_list, place_tagged};
|
||||
|
||||
fn parse(doc: &Document, d: &Dict) -> Option<Content> {
|
||||
let r = nums(doc, d.get(b"Rect"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::layout::norm;
|
||||
use super::*;
|
||||
use super::{Content, MarkKind, ObjRef, Std14};
|
||||
|
||||
pub(super) const TAG: &str = "Added";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::place::{
|
||||
END, Isolation, add_page_resources, add_resources, begin, check, check_on_top, contents, free_font_name, isolation, page_fonts, page_list, place,
|
||||
place_inside_wrapper, stream_tag, text_op, undrawable, wrapper_role,
|
||||
};
|
||||
use super::*;
|
||||
use super::{Document, EditError, Object, added, helvetica_width};
|
||||
|
||||
/// Add a header and footer to `pages` (0-based). With `replace`, existing headers and footers on
|
||||
/// those pages are removed first (Acrobat's Replace Existing).
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Document, EditError, Object, Rgb, added};
|
||||
|
||||
/// The kinds of page marks.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::marks::{MarkKind, matrix_number, n};
|
||||
use super::*;
|
||||
use super::{Dict, Document, EditError, ObjRef, Object, Stream, literal, win_ansi};
|
||||
|
||||
pub(crate) fn page_list(doc: &Document) -> Vec<pdfcraft_model::Page> {
|
||||
pdfcraft_model::pages(doc)
|
||||
|
||||
+118
-75
@@ -6664,12 +6664,124 @@ fn actual_text_pairs_past_the_marked_content_cap() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #2992: the colour a page is painted with before anything else is its background, for exports
|
||||
/// that keep a page colour. Edit a PDF ▸ Background's fill is found under the text it goes
|
||||
/// behind; a page that paints anything else first, a fill that doesn't cover the page, a
|
||||
/// translucent or clipped one, or a colour that isn't plain gray, RGB or CMYK has none.
|
||||
fn page_background_from_content(content: &str, resources: &str) -> Option<[f64; 3]> {
|
||||
let objects = [
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /CropBox [0 0 500 700] /Contents 4 0 R /Resources << {resources} >> >>"),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||||
];
|
||||
let mut bytes = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (index, object) in objects.iter().enumerate() {
|
||||
offsets.push(bytes.len());
|
||||
bytes.extend_from_slice(format!("{} 0 obj\n{object}\nendobj\n", index + 1).as_bytes());
|
||||
}
|
||||
let xref_offset = bytes.len();
|
||||
bytes.extend_from_slice(b"xref\n0 5\n0000000000 65535 f \n");
|
||||
for offset in offsets {
|
||||
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
bytes.extend_from_slice(format!("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{xref_offset}\n%%EOF\n").as_bytes());
|
||||
page_background(&Document::open(Arc::new(bytes)).unwrap(), 0).unwrap()
|
||||
}
|
||||
|
||||
fn assert_page_background_fill_cases(gs: &str) {
|
||||
assert_eq!(page_background_from_content("1 g 0 0 600 800 re f", ""), Some([1.0; 3]), "a producer's white page fill");
|
||||
assert_eq!(page_background_from_content("0 0 1 0 k 0 0 500 700 re f", ""), Some([1.0, 1.0, 0.0]), "CMYK, covering the crop box");
|
||||
assert_eq!(page_background_from_content("/DeviceRGB cs 1 0 0 sc 0 0 500 700 re f", ""), Some([1.0, 0.0, 0.0]), "cs and sc");
|
||||
assert_eq!(page_background_from_content("/Full gs q 2 0 0 2 0 0 cm 0 1 0 rg 0 0 300 400 re f Q", gs), Some([0.0, 1.0, 0.0]), "under a cm");
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 0 0 300 800 re f", ""), None, "half the page");
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 0 0 300 800 re 300 0 300 800 re f", ""), None, "two halves are not one covering rectangle");
|
||||
assert_eq!(
|
||||
page_background_from_content("1 0 0 rg 0 0 300 800 re 0 0 600 800 re f", ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"one of the rectangles covers it"
|
||||
);
|
||||
// #2994 review: a second rectangle that cuts a hole: any one under even-odd, one wound the
|
||||
// other way under nonzero.
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 0 0 600 800 re 100 100 200 200 re f*", ""), None, "even-odd hole");
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 0 0 600 800 re 300 100 -200 200 re f", ""), None, "nonzero, reversed inner rectangle");
|
||||
assert_eq!(
|
||||
page_background_from_content("1 0 0 rg 0 0 600 800 re 100 100 200 200 re f", ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"nonzero, same winding: no hole"
|
||||
);
|
||||
assert_eq!(
|
||||
page_background_from_content("1 0 0 rg q -1 0 0 1 600 0 cm 0 0 600 800 re Q 100 100 200 200 re f", ""),
|
||||
None,
|
||||
"reversed by a mirroring cm"
|
||||
);
|
||||
assert_eq!(page_background_from_content("/Half gs 1 0 0 rg 0 0 600 800 re f", gs), None, "translucent");
|
||||
assert_eq!(page_background_from_content("/Mul gs 1 0 0 rg 0 0 600 800 re f", gs), None, "blended");
|
||||
assert_eq!(page_background_from_content("0 0 100 100 re W n 1 0 0 rg 0 0 600 800 re f", ""), None, "clipped to a corner");
|
||||
assert_eq!(page_background_from_content("0 0 600 800 re W n 1 0 0 rg 0 0 600 800 re f", ""), Some([1.0, 0.0, 0.0]), "a page-sized clip");
|
||||
assert_eq!(page_background_from_content("/Pattern cs /P1 scn 0 0 600 800 re f", ""), None, "a pattern");
|
||||
assert_eq!(page_background_from_content("BT ET 0 0 600 800 re S 1 0 0 rg 0 0 600 800 re f", ""), None, "a stroke first");
|
||||
assert_eq!(page_background_from_content("0 0 m 600 0 l 600 800 l 0 800 l h f", ""), None, "a path that isn't a rectangle");
|
||||
assert_eq!(
|
||||
page_background_from_content("1 0 0 rg 0 0 600 800 re 100 100 m 200 100 l 200 200 l h f*", ""),
|
||||
None,
|
||||
"a page fill with a hole cut in it"
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_page_background_transform_and_clip_cases() {
|
||||
// #2994 review: a rectangle turned 45° about the crop box's centre covers it only if the crop
|
||||
// box's corners are inside it; its bounding box covering the crop box isn't enough. This one
|
||||
// reaches 500 from the centre along each axis (its box covers the 500 × 700 crop box) but
|
||||
// 354 along the diagonals, short of the corners. One reaching 707 covers them.
|
||||
let turned = "0.70710678 0.70710678 -0.70710678 0.70710678 250 350 cm";
|
||||
assert_eq!(
|
||||
page_background_from_content(&format!("1 0 0 rg {turned} -353.553 -353.553 707.107 707.107 re f"), ""),
|
||||
None,
|
||||
"a turned fill short of the corners"
|
||||
);
|
||||
assert_eq!(
|
||||
page_background_from_content(&format!("1 0 0 rg {turned} -500 -500 1000 1000 re f"), ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"a turned fill over them"
|
||||
);
|
||||
// The same for a clip.
|
||||
assert_eq!(
|
||||
page_background_from_content(&format!("{turned} -353.553 -353.553 707.107 707.107 re W n 1 0 0 rg -2000 -2000 4000 4000 re f"), ""),
|
||||
None,
|
||||
"a turned clip"
|
||||
);
|
||||
assert_eq!(
|
||||
page_background_from_content(&format!("{turned} -500 -500 1000 1000 re W n 1 0 0 rg -2000 -2000 4000 4000 re f"), ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"a turned clip over the corners"
|
||||
);
|
||||
// Clips add up: two that each cover the page leave it covered, one that doesn't among them
|
||||
// doesn't.
|
||||
assert_eq!(
|
||||
page_background_from_content("0 0 600 800 re W n 0 0 550 750 re W n 1 0 0 rg 0 0 600 800 re f", ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"two covering clips"
|
||||
);
|
||||
assert_eq!(
|
||||
page_background_from_content("0 0 600 800 re W n 0 0 300 750 re W n 1 0 0 rg 0 0 600 800 re f", ""),
|
||||
None,
|
||||
"the second clip doesn't cover"
|
||||
);
|
||||
// Past 64 clipping rectangles the clip counts as unknown.
|
||||
let clips = "0 0 600 800 re W n ".repeat(65);
|
||||
assert_eq!(page_background_from_content(&format!("{clips}1 0 0 rg 0 0 600 800 re f"), ""), None, "too many clips to follow");
|
||||
// A rectangle drawn the other way round (negative width) covers the page all the same.
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 600 0 -600 800 re f", ""), Some([1.0, 0.0, 0.0]), "drawn clockwise");
|
||||
// The crop box counts as covered within half a percent of its size (2.5 × 3.5 here), the
|
||||
// edge of that included.
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 2 3 496 694 re f", ""), Some([1.0, 0.0, 0.0]), "within the tolerance");
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 2.5 3.5 495 693 re f", ""), Some([1.0, 0.0, 0.0]), "on the tolerance's edge");
|
||||
assert_eq!(page_background_from_content("1 0 0 rg 3 4 494 692 re f", ""), None, "past the tolerance");
|
||||
}
|
||||
|
||||
/// #2992: page background detection follows the first full-page fill under text and ignores
|
||||
/// partial, translucent, clipped, or unsupported fills.
|
||||
#[test]
|
||||
fn a_page_painted_first_with_a_full_page_fill_has_that_background() {
|
||||
let gs = "/ExtGState << /Half << /ca 0.5 >> /Mul << /BM /Multiply >> /Full << /ca 1 /SMask /None >> >>";
|
||||
let mut doc = fixture();
|
||||
assert_eq!(page_background(&doc, 0).unwrap(), None, "text first");
|
||||
add_background(&mut doc, &[0, 1], &Background { color: [0.0, 0.5, 0.5], opacity: 1.0, ..Background::default() }, false).unwrap();
|
||||
@@ -6680,77 +6792,8 @@ fn a_page_painted_first_with_a_full_page_fill_has_that_background() {
|
||||
assert_eq!(page_background(&doc, 2).unwrap(), None, "a page with no content");
|
||||
assert!(page_background(&doc, 9).is_err());
|
||||
|
||||
let page = |content: &str, resources: &str| {
|
||||
let objs: Vec<String> = vec![
|
||||
"<< /Type /Catalog /Pages 2 0 R >>".into(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".into(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 800] /CropBox [0 0 500 700] /Contents 4 0 R /Resources << {resources} >> >>"),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offs = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offs.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let x = out.len();
|
||||
out.extend_from_slice(b"xref\n0 5\n0000000000 65535 f \n");
|
||||
for o in offs {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size 5 /Root 1 0 R >>\nstartxref\n{x}\n%%EOF\n").as_bytes());
|
||||
page_background(&Document::open(Arc::new(out)).unwrap(), 0).unwrap()
|
||||
};
|
||||
let gs = "/ExtGState << /Half << /ca 0.5 >> /Mul << /BM /Multiply >> /Full << /ca 1 /SMask /None >> >>";
|
||||
assert_eq!(page("1 g 0 0 600 800 re f", ""), Some([1.0; 3]), "a producer's white page fill");
|
||||
assert_eq!(page("0 0 1 0 k 0 0 500 700 re f", ""), Some([1.0, 1.0, 0.0]), "CMYK, covering the crop box");
|
||||
assert_eq!(page("/DeviceRGB cs 1 0 0 sc 0 0 500 700 re f", ""), Some([1.0, 0.0, 0.0]), "cs and sc");
|
||||
assert_eq!(page("/Full gs q 2 0 0 2 0 0 cm 0 1 0 rg 0 0 300 400 re f Q", gs), Some([0.0, 1.0, 0.0]), "under a cm");
|
||||
assert_eq!(page("1 0 0 rg 0 0 300 800 re f", ""), None, "half the page");
|
||||
assert_eq!(page("1 0 0 rg 0 0 300 800 re 300 0 300 800 re f", ""), None, "two halves are not one covering rectangle");
|
||||
assert_eq!(page("1 0 0 rg 0 0 300 800 re 0 0 600 800 re f", ""), Some([1.0, 0.0, 0.0]), "one of the rectangles covers it");
|
||||
// #2994 review: a second rectangle that cuts a hole: any one under even-odd, one wound the
|
||||
// other way under nonzero.
|
||||
assert_eq!(page("1 0 0 rg 0 0 600 800 re 100 100 200 200 re f*", ""), None, "even-odd hole");
|
||||
assert_eq!(page("1 0 0 rg 0 0 600 800 re 300 100 -200 200 re f", ""), None, "nonzero, reversed inner rectangle");
|
||||
assert_eq!(page("1 0 0 rg 0 0 600 800 re 100 100 200 200 re f", ""), Some([1.0, 0.0, 0.0]), "nonzero, same winding: no hole");
|
||||
assert_eq!(page("1 0 0 rg q -1 0 0 1 600 0 cm 0 0 600 800 re Q 100 100 200 200 re f", ""), None, "reversed by a mirroring cm");
|
||||
assert_eq!(page("/Half gs 1 0 0 rg 0 0 600 800 re f", gs), None, "translucent");
|
||||
assert_eq!(page("/Mul gs 1 0 0 rg 0 0 600 800 re f", gs), None, "blended");
|
||||
assert_eq!(page("0 0 100 100 re W n 1 0 0 rg 0 0 600 800 re f", ""), None, "clipped to a corner");
|
||||
assert_eq!(page("0 0 600 800 re W n 1 0 0 rg 0 0 600 800 re f", ""), Some([1.0, 0.0, 0.0]), "a page-sized clip");
|
||||
assert_eq!(page("/Pattern cs /P1 scn 0 0 600 800 re f", ""), None, "a pattern");
|
||||
assert_eq!(page("BT ET 0 0 600 800 re S 1 0 0 rg 0 0 600 800 re f", ""), None, "a stroke first");
|
||||
assert_eq!(page("0 0 m 600 0 l 600 800 l 0 800 l h f", ""), None, "a path that isn't a rectangle");
|
||||
assert_eq!(page("1 0 0 rg 0 0 600 800 re 100 100 m 200 100 l 200 200 l h f*", ""), None, "a page fill with a hole cut in it");
|
||||
// #2994 review: a rectangle turned 45° about the crop box's centre covers it only if the crop
|
||||
// box's corners are inside it; its bounding box covering the crop box isn't enough. This one
|
||||
// reaches 500 from the centre along each axis (its box covers the 500 × 700 crop box) but
|
||||
// 354 along the diagonals, short of the corners. One reaching 707 covers them.
|
||||
let turned = "0.70710678 0.70710678 -0.70710678 0.70710678 250 350 cm";
|
||||
assert_eq!(page(&format!("1 0 0 rg {turned} -353.553 -353.553 707.107 707.107 re f"), ""), None, "a turned fill short of the corners");
|
||||
assert_eq!(page(&format!("1 0 0 rg {turned} -500 -500 1000 1000 re f"), ""), Some([1.0, 0.0, 0.0]), "a turned fill over them");
|
||||
// The same for a clip.
|
||||
assert_eq!(page(&format!("{turned} -353.553 -353.553 707.107 707.107 re W n 1 0 0 rg -2000 -2000 4000 4000 re f"), ""), None, "a turned clip");
|
||||
assert_eq!(
|
||||
page(&format!("{turned} -500 -500 1000 1000 re W n 1 0 0 rg -2000 -2000 4000 4000 re f"), ""),
|
||||
Some([1.0, 0.0, 0.0]),
|
||||
"a turned clip over the corners"
|
||||
);
|
||||
// Clips add up: two that each cover the page leave it covered, one that doesn't among them
|
||||
// doesn't.
|
||||
assert_eq!(page("0 0 600 800 re W n 0 0 550 750 re W n 1 0 0 rg 0 0 600 800 re f", ""), Some([1.0, 0.0, 0.0]), "two covering clips");
|
||||
assert_eq!(page("0 0 600 800 re W n 0 0 300 750 re W n 1 0 0 rg 0 0 600 800 re f", ""), None, "the second clip doesn't cover");
|
||||
// Past 64 clipping rectangles the clip counts as unknown.
|
||||
let clips = "0 0 600 800 re W n ".repeat(65);
|
||||
assert_eq!(page(&format!("{clips}1 0 0 rg 0 0 600 800 re f"), ""), None, "too many clips to follow");
|
||||
// A rectangle drawn the other way round (negative width) covers the page all the same.
|
||||
assert_eq!(page("1 0 0 rg 600 0 -600 800 re f", ""), Some([1.0, 0.0, 0.0]), "drawn clockwise");
|
||||
// The crop box counts as covered within half a percent of its size (2.5 × 3.5 here), the
|
||||
// edge of that included.
|
||||
assert_eq!(page("1 0 0 rg 2 3 496 694 re f", ""), Some([1.0, 0.0, 0.0]), "within the tolerance");
|
||||
assert_eq!(page("1 0 0 rg 2.5 3.5 495 693 re f", ""), Some([1.0, 0.0, 0.0]), "on the tolerance's edge");
|
||||
assert_eq!(page("1 0 0 rg 3 4 494 692 re f", ""), None, "past the tolerance");
|
||||
assert_page_background_fill_cases(gs);
|
||||
assert_page_background_transform_and_clip_cases();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::interp::{Carry, interpret};
|
||||
use super::lines::{content_streams, page_dict, reading_lines, rewritten_contents, splice, text_lines};
|
||||
use super::rewrite::{LineEdit, coincident_ops, rewrite_block};
|
||||
use super::*;
|
||||
use super::{Document, EditError, HashMap, Object, Op, Pieces, TextLine, group_blocks, regions};
|
||||
|
||||
/// An editable text region: adjacent runs on visual rows, followed by aligned rows.
|
||||
/// A region may contain several source fonts, sizes, colours and weights.
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::blocks::{BlockStyle, TextBlock};
|
||||
use super::fallback::MAX_TYPE3_GLYPHS;
|
||||
use super::lines::{page_dict, text_lines};
|
||||
use super::rewrite::rewrite_block_from;
|
||||
use super::*;
|
||||
use super::{DecodeBudget, Document, EditError, Metrics, Object, TextLine, TextSession};
|
||||
|
||||
/// An embedded sfnt source face, read only for the inline editor. Bytes stay in memory and
|
||||
/// are never installed, copied into assets, or used as a font for another document.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use super::interp::Ts;
|
||||
use super::lines::page_dict;
|
||||
use super::*;
|
||||
use super::{
|
||||
CraftFont, Dict, Document, EditError, GlyphError, HashMap, Metrics, Object, Op, PdfString, Stream, TextLine, japanese_glyph_from, parse,
|
||||
};
|
||||
|
||||
/// Text → the runs that show it: each run's font resource name and the bytes shown in that font.
|
||||
pub(super) type Encoder = Box<dyn Fn(&str) -> Option<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
+283
-249
@@ -1,6 +1,6 @@
|
||||
use super::fallback::is_type3_fallback;
|
||||
use super::rewrite::vertical_advance;
|
||||
use super::*;
|
||||
use super::{Dict, Document, EditError, HashMap, Matrix, Metrics, Object, Op, Rc};
|
||||
|
||||
/// Text and graphics state captured for rewriting.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -472,6 +472,280 @@ 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);
|
||||
|
||||
#[derive(Default)]
|
||||
struct MarkedContent {
|
||||
regions: Vec<Option<usize>>,
|
||||
layers: Vec<Option<Vec<u8>>>,
|
||||
actual_text: Vec<Option<(usize, usize, String)>>,
|
||||
}
|
||||
|
||||
impl MarkedContent {
|
||||
fn begin(&mut self, doc: &Document, op: &Op, index: usize, logical_text: Option<&Dict>, shown_count: usize) {
|
||||
// Marked text paragraphs/headings are authoritative boundaries. Ignore artifacts and
|
||||
// inner spans, which inherit their enclosing region.
|
||||
let region = matches!(op.name(0), Some(b"P" | b"H" | b"H1" | b"H2" | b"H3" | b"H4" | b"H5" | b"H6" | b"LI" | b"TD" | b"TH"));
|
||||
self.regions.push(if region { Some(index) } else { self.regions.last().copied().flatten() });
|
||||
// A layer named in the resources is told apart by its name, an inline one by where it opens.
|
||||
let layer = op.is("BDC") && op.name(0) == Some(b"OC".as_slice());
|
||||
self.layers
|
||||
.push(layer.then(|| op.operands.get(1).and_then(Object::as_name).map_or_else(|| format!("@{index}").into_bytes(), <[u8]>::to_vec)));
|
||||
// The property list is inline or named in the resources' /Properties (§14.6.2).
|
||||
let list = logical_text.and_then(|properties| match op.operands.get(1) {
|
||||
Some(Object::Name(name)) => properties.get(name).and_then(|object| doc.dict(object)),
|
||||
other => other.and_then(Object::as_dict).cloned(),
|
||||
});
|
||||
let logical = list.as_ref().and_then(|dict| dict.get(b"ActualText")).and_then(|value| match value {
|
||||
Object::String(value) if value.bytes.len() <= 4_000_000 => Some(value.to_text()),
|
||||
_ => None,
|
||||
});
|
||||
self.actual_text.push(logical.map(|text| (shown_count, index, text)));
|
||||
}
|
||||
|
||||
fn end(&mut self, index: usize, shown: &mut Vec<Shown>) {
|
||||
self.regions.pop();
|
||||
self.layers.pop();
|
||||
if let Some(Some((start, open, logical))) = self.actual_text.pop() {
|
||||
let tail = shown.split_off(start.min(shown.len()));
|
||||
let mut items = tail.into_iter();
|
||||
if let Some(mut first) = items.next() {
|
||||
for item in items {
|
||||
first.glyph_count = first.glyph_count.saturating_add(item.glyph_count);
|
||||
first.extra_ops.push(item.op);
|
||||
first.extra_ops.extend(item.extra_ops);
|
||||
first.rect = [
|
||||
first.rect[0].min(item.rect[0]),
|
||||
first.rect[1].min(item.rect[1]),
|
||||
first.rect[2].max(item.rect[2]),
|
||||
first.rect[3].max(item.rect[3]),
|
||||
];
|
||||
first.frame = union(first.frame, item.frame);
|
||||
first.ink.extend(item.ink);
|
||||
}
|
||||
first.extra_ops.extend([open, index]);
|
||||
// Only right-to-left ActualText is already in typing order and must stay out of
|
||||
// bidi recovery. Word and Chrome also wrap Latin ligatures mid-word.
|
||||
first.logical_order = logical.chars().any(crate::bidi::is_rtl);
|
||||
first.actual_text = true;
|
||||
first.text = logical;
|
||||
first.start_x = first.rect[0];
|
||||
first.end_x = first.rect[2];
|
||||
first.end_y = first.baseline;
|
||||
first.advance = (first.end_x - first.start_x) / first.tm.then(&first.state.ctm).0[0].abs().max(1e-6);
|
||||
shown.push(first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TextRunState<'a> {
|
||||
ts: &'a mut Ts,
|
||||
tm: &'a mut Matrix,
|
||||
tlm: &'a mut Matrix,
|
||||
at_bt: &'a (usize, Ts),
|
||||
bt: usize,
|
||||
marked: &'a MarkedContent,
|
||||
carry: &'a mut Carry,
|
||||
out: &'a mut Vec<Shown>,
|
||||
}
|
||||
|
||||
fn show_text(op: &Op, i: usize, state: &mut TextRunState<'_>) -> Result<(), EditError> {
|
||||
let ts = &mut *state.ts;
|
||||
let tm = &mut *state.tm;
|
||||
let tlm = &mut *state.tlm;
|
||||
let at_bt = state.at_bt;
|
||||
let bt = state.bt;
|
||||
let marked = state.marked;
|
||||
let carry = &mut *state.carry;
|
||||
let out = &mut *state.out;
|
||||
if matches!(op.op.as_slice(), b"'" | b"\"") {
|
||||
if op.is("\"") {
|
||||
ts.word_spacing = op.num(0).unwrap_or(ts.word_spacing);
|
||||
ts.char_spacing = op.num(1).unwrap_or(ts.char_spacing);
|
||||
}
|
||||
*tlm = Matrix([1.0, 0.0, 0.0, 1.0, 0.0, -ts.leading]).then(tlm);
|
||||
*tm = *tlm;
|
||||
}
|
||||
let Some((name, m)) = ts.font.clone() else { return Ok(()) };
|
||||
// Pieces: strings, and TJ number adjustments.
|
||||
let pieces: Vec<Object> = match op.op.as_slice() {
|
||||
// A surplus array (`[(A)] [(B)] TJ`) is malformed, but the operator still
|
||||
// takes the one immediately before it, as the renderer and every other
|
||||
// reader here do (#3569) -- not the first, which a reader counting operands
|
||||
// from the front of a surplus list would otherwise take.
|
||||
b"TJ" => op.operands.last().and_then(Object::as_array).cloned().unwrap_or_default(),
|
||||
_ => op.operands.last().cloned().into_iter().collect(),
|
||||
};
|
||||
// Tc is already included in the pen advance; TJ may encode tracking instead.
|
||||
let tracking = if op.is("TJ") { tj_tracking(&pieces, &m, ts.size, ts.scale) } else { 0.0 };
|
||||
let trm0 = tm.then(&ts.ctm);
|
||||
let start = trm0.apply(0.0, ts.rise);
|
||||
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();
|
||||
let mut in_word = false;
|
||||
let mut glyph_count = 0_usize;
|
||||
for p in &pieces {
|
||||
match p {
|
||||
Object::String(s) => {
|
||||
for (code, len) in m.codes(&s.bytes) {
|
||||
glyph_count = glyph_count.saturating_add(1);
|
||||
let shown = m.text_of(code);
|
||||
let blank = m.is_space(code, len) || shown.as_deref().is_some_and(|t| !t.is_empty() && t.chars().all(char::is_whitespace));
|
||||
match shown {
|
||||
Some(t) => {
|
||||
carry.text_left = carry
|
||||
.text_left
|
||||
.checked_sub(t.len())
|
||||
.ok_or_else(|| EditError::Invalid("the page's text is too large to read".into()))?;
|
||||
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:
|
||||
// the word is the union of its glyphs, whichever way they go.
|
||||
let (lo, hi) = (x_text.min(ink_end), x_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;
|
||||
let w = m.width(code) * ts.size + ts.char_spacing + if m.is_space(code, len) { ts.word_spacing } else { 0.0 };
|
||||
x_text += w * ts.scale;
|
||||
}
|
||||
}
|
||||
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 - tracking > (ts.size * ts.scale).abs() * 0.2 && !text.ends_with(' ') {
|
||||
text.push(' ');
|
||||
}
|
||||
// Tracking and kerning keep a word together; a larger jump
|
||||
// ends it (whatever the sign of the size or `Tz`).
|
||||
if dx - tracking > (ts.size * ts.scale).abs() * 0.2 || dx < -(ts.size * ts.scale).abs() * 0.2 {
|
||||
in_word = false;
|
||||
}
|
||||
x_text += dx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*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 `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)]);
|
||||
// The same corners along and across the baseline: the run's own box, not its
|
||||
// upright bounding box turned (which is wider by the box's height on a slant).
|
||||
let dir = direction(&trm0);
|
||||
let frame = corners
|
||||
.iter()
|
||||
.map(|&p| to_frame(dir, p))
|
||||
.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)]);
|
||||
let ink = words
|
||||
.iter()
|
||||
.map(|&(a, b)| {
|
||||
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)])
|
||||
})
|
||||
.collect();
|
||||
let size_user = (trm0.0[2].powi(2) + trm0.0[3].powi(2)).sqrt() * ts.size;
|
||||
// A numeric-only TJ adjusts the current text matrix but paints no glyphs.
|
||||
// It must not create a phantom editable line or enlarge its neighbours' boxes.
|
||||
if !pieces.iter().any(|p| matches!(p, Object::String(s) if !s.bytes.is_empty())) {
|
||||
return Ok(());
|
||||
}
|
||||
out.push(Shown {
|
||||
op: i,
|
||||
extra_ops: Vec::new(),
|
||||
glyph_count,
|
||||
// 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 },
|
||||
tracking: 0.0,
|
||||
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: *tlm,
|
||||
bt_op: at_bt.0,
|
||||
bt_state: at_bt.1.clone(),
|
||||
text,
|
||||
rect,
|
||||
frame,
|
||||
ink,
|
||||
baseline: start.1,
|
||||
start_x: start.0,
|
||||
end_x: end.0,
|
||||
end_y: end.1,
|
||||
bt,
|
||||
font: name,
|
||||
base_font: m.base_font.clone(),
|
||||
face: ts.face.clone(),
|
||||
size: if size_user > 0.0 { size_user } else { ts.size },
|
||||
bold: m.bold,
|
||||
italic: m.italic,
|
||||
visual_order: m.visual_order,
|
||||
decodable,
|
||||
scope: marked.regions.last().copied().flatten(),
|
||||
paint: Paint { layers: marked.layers.iter().flatten().cloned().collect(), gs: ts.gs.clone() },
|
||||
logical_order: false,
|
||||
actual_text: false,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn interpret(
|
||||
doc: &Document,
|
||||
ops: &[Op],
|
||||
@@ -490,66 +764,17 @@ pub(super) fn interpret(
|
||||
let mut stack: Vec<Ts> = std::mem::take(&mut carry.stack);
|
||||
let (mut tm, mut tlm) = (Matrix::IDENTITY, Matrix::IDENTITY);
|
||||
let mut bt = 0usize;
|
||||
let mut marked = std::mem::take(&mut carry.marked);
|
||||
let mut layers = std::mem::take(&mut carry.layers);
|
||||
let mut actual: Vec<Option<(usize, usize, String)>> = Vec::new();
|
||||
let mut marked =
|
||||
MarkedContent { regions: std::mem::take(&mut carry.marked), layers: std::mem::take(&mut carry.layers), ..MarkedContent::default() };
|
||||
for (i, op) in ops.iter().enumerate() {
|
||||
match op.op.as_slice() {
|
||||
// No depth cap: every `EMC` pops, so a capped push would pair it with the enclosing
|
||||
// sequence (#3573). Both stacks stay bounded by the number of operators.
|
||||
b"BMC" | b"BDC" => {
|
||||
// Marked text paragraphs/headings are authoritative boundaries. Ignore
|
||||
// artifacts and inner spans, which inherit their enclosing region.
|
||||
let region = matches!(op.name(0), Some(b"P" | b"H" | b"H1" | b"H2" | b"H3" | b"H4" | b"H5" | b"H6" | b"LI" | b"TD" | b"TH"));
|
||||
marked.push(if region { Some(i) } else { marked.last().copied().flatten() });
|
||||
// A layer named in the resources is told apart by its name, an inline one by where it opens.
|
||||
let layer = op.is("BDC") && op.name(0) == Some(b"OC".as_slice());
|
||||
layers.push(layer.then(|| op.operands.get(1).and_then(Object::as_name).map_or_else(|| format!("@{i}").into_bytes(), <[u8]>::to_vec)));
|
||||
// The property list is inline or named in the resources' /Properties (§14.6.2).
|
||||
let list = logical_text.and_then(|properties| match op.operands.get(1) {
|
||||
Some(Object::Name(n)) => properties.get(n).and_then(|o| doc.dict(o)),
|
||||
other => other.and_then(Object::as_dict).cloned(),
|
||||
});
|
||||
let logical = list.as_ref().and_then(|d| d.get(b"ActualText")).and_then(|s| match s {
|
||||
Object::String(s) if s.bytes.len() <= 4_000_000 => Some(s.to_text()),
|
||||
_ => None,
|
||||
});
|
||||
actual.push(logical.map(|s| (out.len(), i, s)));
|
||||
marked.begin(doc, op, i, logical_text, out.len());
|
||||
}
|
||||
b"EMC" => {
|
||||
marked.pop();
|
||||
layers.pop();
|
||||
if let Some(Some((start, open, logical))) = actual.pop() {
|
||||
let tail = out.split_off(start.min(out.len()));
|
||||
let mut items = tail.into_iter();
|
||||
if let Some(mut first) = items.next() {
|
||||
for shown in items {
|
||||
first.glyph_count = first.glyph_count.saturating_add(shown.glyph_count);
|
||||
first.extra_ops.push(shown.op);
|
||||
first.extra_ops.extend(shown.extra_ops);
|
||||
first.rect = [
|
||||
first.rect[0].min(shown.rect[0]),
|
||||
first.rect[1].min(shown.rect[1]),
|
||||
first.rect[2].max(shown.rect[2]),
|
||||
first.rect[3].max(shown.rect[3]),
|
||||
];
|
||||
first.frame = union(first.frame, shown.frame);
|
||||
first.ink.extend(shown.ink);
|
||||
}
|
||||
first.extra_ops.extend([open, i]);
|
||||
// Only right-to-left ActualText is already in typing order and must stay
|
||||
// out of the line's bidi recovery. Word and Chrome wrap Latin ligatures
|
||||
// (`fi`, `fl`) in ActualText spans mid-word; those keep joining their line.
|
||||
first.logical_order = logical.chars().any(crate::bidi::is_rtl);
|
||||
first.actual_text = true;
|
||||
first.text = logical;
|
||||
first.start_x = first.rect[0];
|
||||
first.end_x = first.rect[2];
|
||||
first.end_y = first.baseline;
|
||||
first.advance = (first.end_x - first.start_x) / first.tm.then(&first.state.ctm).0[0].abs().max(1e-6);
|
||||
out.push(first);
|
||||
}
|
||||
}
|
||||
marked.end(i, &mut out);
|
||||
}
|
||||
b"q" => stack.push(ts.clone()),
|
||||
b"Q" => {
|
||||
@@ -650,207 +875,16 @@ pub(super) fn interpret(
|
||||
tm = tlm;
|
||||
}
|
||||
b"Tj" | b"TJ" | b"'" | b"\"" => {
|
||||
if matches!(op.op.as_slice(), b"'" | b"\"") {
|
||||
if op.is("\"") {
|
||||
ts.word_spacing = op.num(0).unwrap_or(ts.word_spacing);
|
||||
ts.char_spacing = op.num(1).unwrap_or(ts.char_spacing);
|
||||
}
|
||||
tlm = Matrix([1.0, 0.0, 0.0, 1.0, 0.0, -ts.leading]).then(&tlm);
|
||||
tm = tlm;
|
||||
}
|
||||
let Some((name, m)) = ts.font.clone() else { continue };
|
||||
// Pieces: strings, and TJ number adjustments.
|
||||
let pieces: Vec<Object> = match op.op.as_slice() {
|
||||
// A surplus array (`[(A)] [(B)] TJ`) is malformed, but the operator still
|
||||
// takes the one immediately before it, as the renderer and every other
|
||||
// reader here do (#3569) -- not the first, which a reader counting operands
|
||||
// from the front of a surplus list would otherwise take.
|
||||
b"TJ" => op.operands.last().and_then(Object::as_array).cloned().unwrap_or_default(),
|
||||
_ => op.operands.last().cloned().into_iter().collect(),
|
||||
};
|
||||
// Tc is already included in the pen advance; TJ may encode tracking instead.
|
||||
let tracking = if op.is("TJ") { tj_tracking(&pieces, &m, ts.size, ts.scale) } else { 0.0 };
|
||||
let trm0 = tm.then(&ts.ctm);
|
||||
let start = trm0.apply(0.0, ts.rise);
|
||||
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();
|
||||
let mut in_word = false;
|
||||
let mut glyph_count = 0_usize;
|
||||
for p in &pieces {
|
||||
match p {
|
||||
Object::String(s) => {
|
||||
for (code, len) in m.codes(&s.bytes) {
|
||||
glyph_count = glyph_count.saturating_add(1);
|
||||
let shown = m.text_of(code);
|
||||
let blank =
|
||||
m.is_space(code, len) || shown.as_deref().is_some_and(|t| !t.is_empty() && t.chars().all(char::is_whitespace));
|
||||
match shown {
|
||||
Some(t) => {
|
||||
carry.text_left = carry
|
||||
.text_left
|
||||
.checked_sub(t.len())
|
||||
.ok_or_else(|| EditError::Invalid("the page's text is too large to read".into()))?;
|
||||
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:
|
||||
// the word is the union of its glyphs, whichever way they go.
|
||||
let (lo, hi) = (x_text.min(ink_end), x_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;
|
||||
let w = m.width(code) * ts.size + ts.char_spacing + if m.is_space(code, len) { ts.word_spacing } else { 0.0 };
|
||||
x_text += w * ts.scale;
|
||||
}
|
||||
}
|
||||
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 - tracking > (ts.size * ts.scale).abs() * 0.2 && !text.ends_with(' ') {
|
||||
text.push(' ');
|
||||
}
|
||||
// Tracking and kerning keep a word together; a larger jump
|
||||
// ends it (whatever the sign of the size or `Tz`).
|
||||
if dx - tracking > (ts.size * ts.scale).abs() * 0.2 || dx < -(ts.size * ts.scale).abs() * 0.2 {
|
||||
in_word = false;
|
||||
}
|
||||
x_text += dx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 `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)]);
|
||||
// The same corners along and across the baseline: the run's own box, not its
|
||||
// upright bounding box turned (which is wider by the box's height on a slant).
|
||||
let dir = direction(&trm0);
|
||||
let frame = corners
|
||||
.iter()
|
||||
.map(|&p| to_frame(dir, p))
|
||||
.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)]);
|
||||
let ink = words
|
||||
.iter()
|
||||
.map(|&(a, b)| {
|
||||
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)])
|
||||
})
|
||||
.collect();
|
||||
let size_user = (trm0.0[2].powi(2) + trm0.0[3].powi(2)).sqrt() * ts.size;
|
||||
// A numeric-only TJ adjusts the current text matrix but paints no glyphs.
|
||||
// It must not create a phantom editable line or enlarge its neighbours' boxes.
|
||||
if !pieces.iter().any(|p| matches!(p, Object::String(s) if !s.bytes.is_empty())) {
|
||||
continue;
|
||||
}
|
||||
out.push(Shown {
|
||||
op: i,
|
||||
extra_ops: Vec::new(),
|
||||
glyph_count,
|
||||
// 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 },
|
||||
tracking: 0.0,
|
||||
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,
|
||||
bt_state: at_bt.1.clone(),
|
||||
text,
|
||||
rect,
|
||||
frame,
|
||||
ink,
|
||||
baseline: start.1,
|
||||
start_x: start.0,
|
||||
end_x: end.0,
|
||||
end_y: end.1,
|
||||
bt,
|
||||
font: name,
|
||||
base_font: m.base_font.clone(),
|
||||
face: ts.face.clone(),
|
||||
size: if size_user > 0.0 { size_user } else { ts.size },
|
||||
bold: m.bold,
|
||||
italic: m.italic,
|
||||
visual_order: m.visual_order,
|
||||
decodable,
|
||||
scope: marked.last().copied().flatten(),
|
||||
paint: Paint { layers: layers.iter().flatten().cloned().collect(), gs: ts.gs.clone() },
|
||||
logical_order: false,
|
||||
actual_text: false,
|
||||
});
|
||||
let mut state = TextRunState { ts: &mut ts, tm: &mut tm, tlm: &mut tlm, at_bt: &at_bt, bt, marked: &marked, carry, out: &mut out };
|
||||
show_text(op, i, &mut state)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
carry.ts = ts;
|
||||
carry.stack = stack;
|
||||
carry.marked = marked;
|
||||
carry.layers = layers;
|
||||
carry.marked = marked.regions;
|
||||
carry.layers = marked.layers;
|
||||
positioned_tracking(&mut out);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::fallback::{same_font, same_paint};
|
||||
use super::interp::{Carry, Origin, Pen, Shown, TextState, Ts, direction, interpret, to_frame, union};
|
||||
use super::*;
|
||||
use super::{DecodeBudget, Dict, Document, EditError, HashMap, Matrix, Object, Op, Pieces, Stream, TextLine, parse, serialize_ops};
|
||||
|
||||
impl TextLine {
|
||||
/// The baseline's height in user space.
|
||||
|
||||
+599
-337
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,11 @@
|
||||
//! Geometry-based regions and mixed-style rewriting of existing text.
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
BlockStyle, CraftFont, Dict, Document, EditError, HashMap, LineEdit, MAX_TYPE3_GLYPHS, Matrix, Metrics, Object, Op, PdfString, Pieces,
|
||||
RangeStyle, TextBlock, TextLine, Type3Glyph, coincident_ops, content_streams, fallback_face, japanese_glyph_from, needs_type3, no_fallback_font,
|
||||
no_glyph_for, page_dict, paragraph_separator, reveal_ops, rewritten_contents, same_base_font, same_font, source_encodes, source_family,
|
||||
source_program, source_show, source_space, source_width, splice, strip_subset_tag, type3_faces, type3_glyph, union, write_type3_glyphs,
|
||||
};
|
||||
|
||||
#[path = "text_regions/grouping.rs"]
|
||||
mod grouping;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{HashMap, Matrix, TextBlock, TextLine, paragraph_separator, same_base_font, same_font, strip_subset_tag, union};
|
||||
|
||||
fn same_plane(a: &TextLine, b: &TextLine) -> bool {
|
||||
let at = Matrix(a.origin.tm).then(&Matrix(a.origin.ctm));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,10 @@
|
||||
//! another one's output; the commit writes into the document it was opened on, and refuses if
|
||||
//! that document has changed since.
|
||||
|
||||
use super::*;
|
||||
use super::{
|
||||
BlockStyle, Document, EditError, LineEdit, MAX_TYPE3_GLYPHS, TextBlock, TextEditLayout, TextLine, group_blocks, layout_draft, rewrite_block_from,
|
||||
text_lines,
|
||||
};
|
||||
|
||||
/// One paragraph being edited: the document as it was opened, and the page's lines then.
|
||||
pub struct TextSession {
|
||||
|
||||
Reference in New Issue
Block a user