Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions ATTRIBUTION.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "a3234684d5318ce38213c79533d3eebb9c635497ba9e00fb9ca7ebe2fad9d73d"
sha256 = "92847610119de9e0f3a4748cf643608ef26e5e646841231d2e5238b7bad654a6"

# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
Expand All @@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c5e0206846b009bcb366d092c3d64fd8b9e42d29d5406fb73ae36ecb1f2083d5"
sha256 = "15a47d6a56b74d53e661e14e38a61f8446c727ff21cc5f890df7be04d8f815e2"

[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
Expand All @@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "349a2ead371bdbfeefb9ab2279c676b205cefe163cbef174f8123fe4bf739ab9"
sha256 = "7b09eafbba0dd779576b1f7815965511b51c1706708d570aaaa714c4acfa0ce5"

[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
Expand All @@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "cb332907792d4aa760ca9862160e24a7b469afdbcfb93235e99deb8d58604ae2"
sha256 = "5016f4293aa93a73e0f4ac359e0bcd6f3d2686a73c5c60a497596f43d7bcf372"

[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
Expand All @@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ebd3a12e471ba9d9a69e6532fbac2d027dec3c91b79cdfbb1c6c386d13de3bd6"
sha256 = "6d1a508ff4b3fdd9f96dd7d6d5597c124c93e6237734a151a9b1b2c5500faa52"

[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
Expand All @@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f5e0553165ab0223bfbc809b1630170d1af11d6546ae5a682d4090104b6e4a05"
sha256 = "947762dc40d01c8d4a156b89845301e6b658adf94f92c2225b102099f85b2ad7"

[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
Expand All @@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "dd2171a0a940a84e512fd6408a11cddf104be897ec4ab670c5ef6f1781ccb671"
sha256 = "20f75f2d683b7439c7fa94d1079332685ad0e3c7aa7324223cbe296f09f84fb3"

[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
Expand All @@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1b9b3d0d6dab626c3acb8c9aa64d64e56b98eb3f2faf71dcb1957067e72706db"
sha256 = "670f299ba223b3908c3e3544c673a24102254a07e96b153282b0c3cc7a9032b3"

[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
Expand All @@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "668f23f4095120343e3f0910dc8f5844765370fae40400c48d685cad20dcf60e"
sha256 = "4663e2e30c09b35dddacf368ce4deb7eb329a740e3c7b15dbf1ba0bfe8bb9f09"

[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
Expand All @@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "02f3fd4b2721b0ea9c01c99d477a0339a28180514568179d3fc420e7fb81c671"
sha256 = "6eaafc4b73cc4c01ab984077c53f6fbf5049f86703617fd0b9b5661f3cc031b1"

[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
Expand All @@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "26eb438c309d34e55eba959a21f139db91fa5fd1d6d4b7c3d1cd0fa79a60a5b0"
sha256 = "001629f9c5ef40b90bda4403774b7ac1325f7888627b9ed667c3eb2e9742fd0f"

[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
Expand All @@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c04b6d0320bce999b33bd189bffcec8a196d4169e56cc65675dc920c5a671dac"
sha256 = "51b8ad57d14db5ff021597b6f28fd93e9ab07a1f4ef169993d748a15d01f2bb2"

[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
Expand All @@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "37b042977d934b0b550e5f52faa5d90abbcb9e7ff61ff9588021610ce709e474"
sha256 = "52ee2ff3d7d99732b43a553a72434ee4cfa16e1e9117b8eadefef5652193cbdd"

[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
Expand All @@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "3d1f1d2558eec1271fa653bc38616feeefc6559deeb2a0cdbf07d67634e9ac97"
sha256 = "d1dbf0f13f62463b117d3a00ffd3beee646321e788d73bff24984b57b090c92a"

[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
Expand All @@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "03071249bd6c13f79da72262415e484fb1aa36098a37a49094b9dab58cebc4e2"
sha256 = "7f8083d0b1052a9f7b79afce2814f14c390df6629316b045a45b41bedf9168b2"

[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
Expand All @@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c0ba73b27b95b275f5eebc040ab1c3c6c3b227c63e1c637b4cdb27d0a9264a8b"
sha256 = "3965fe52de88440c4373bc3c331955860f45a4d2beb6c273b2954082dc741b3c"

[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
Expand All @@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "e3af76714ee9899328a453ec83195f4dc462fe48ba445fbe3688c777e74bc9dc"
sha256 = "8b881faecb1f5d8b3bf495269f39b7860ebdb4e161e5b6c77a985ef524d8e17d"

[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
Expand All @@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0855d7f785c433799da83400ebc932f987eb543008486bc4f553aadb4102c2ba"
sha256 = "cc03ec5800600619a2769c0f074b48b5e1396bc7405a455328a034b90a09252b"

[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
Expand All @@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "884ab18d60db52f65cd0e30530e196c73c0e627f450bc948517e2f4b7deeb04b"
sha256 = "739c45a6737844a97cef1e31e590b597698842362a8b7502b72763a469451875"

[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
Expand All @@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c805a2c6da08e77a9a7455856d82c168cf6f353d96e1dc140fcc8898f12ba538"
sha256 = "b0a3e2b0bafb2bb28a37359f45c00f3873daa49a8356877e5a77d776c4f28109"


[[asset]]
Expand Down Expand Up @@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "11619064ae9edaeee5d601b7008e40095a40375a3e1a298155d59cb758fe7d34"
sha256 = "ea0b4bc12fd178032888573741c79c513dc40499e17e37924d009e1832dfadfd"

[[asset]]
path = "crates/ui-egui/src/i18n/sl.tsv"
Expand All @@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "89f2c7495cfb0fc9d36d35b95c056dbeda18ccae521250babac719ff5df62d89"
sha256 = "7df9da11c7eb77719b5b2b06e93b7be69ebcc10c840c3311d31d5e47d50b469d"

[[asset]]

Expand Down
1 change: 1 addition & 0 deletions crates/automation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ impl Automation {
size: a.opt_num("size")?,
underline: a.opt_bool("underline")?,
line_spacing: a.opt_num("line_spacing")?,
paragraph_spacing: a.opt_num("paragraph_spacing")?,
char_spacing: a.opt_num("char_spacing")?,
scale: a.opt_num("scale")?,
width: a.opt_num("width")?,
Expand Down
1 change: 1 addition & 0 deletions crates/automation/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,7 @@ pub fn tools() -> Vec<ToolDef> {
"align": { "type": "string", "enum": ["left", "center", "right", "justify"] },
"underline": { "type": "boolean" },
"line_spacing": { "type": "number", "description": "Multiple of the font size (1.2 is ordinary)." },
"paragraph_spacing": { "type": "number", "description": "Points between paragraphs (a line break in text starts one), beyond the line spacing; default half the size." },
"dx": { "type": "number", "description": "Paragraph only: move right by this many points (negative: left)." },
"dy": { "type": "number", "description": "Paragraph only: move up by this many points (negative: down)." },
"width": { "type": "number", "description": "Paragraph only: rewrap to this width in points (dragging the box's edge)." },
Expand Down
35 changes: 35 additions & 0 deletions crates/edit/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4473,3 +4473,38 @@ fn removing_marks_strips_wrappers_that_closed_open_state() {
assert_eq!(remove_marks(&mut doc, &[0], MarkKind::Watermark).unwrap(), 1);
assert_eq!(streams(&doc, 0), ["q 2 0 0 2 0 0 cm"]);
}

/// A line break in the edited text keeps a row of its own (the editor's rows); with a paragraph
/// spacing set, that much more space goes before it. A row the page already had
/// stays where it was, shifted by the spacing added above it.
#[test]
fn paragraph_breaks_get_their_own_spacing() {
let baselines = |doc: &Document| -> Vec<f64> { text::text_lines(&reopen(doc), 0).unwrap().iter().map(|l| l.rect[1]).collect() };
let gap_after_first = |b: &[f64]| b[0] - b[1];
// A one-line source has no row step of its own: 1.5 × the size stands in.
let mut doc = text_page("BT /F1 10 Tf 72 700 Td (The quick brown fox) Tj ET");
text::rewrite_block(&mut doc, 0, 0, Some("First one\nSecond one"), &text::BlockStyle::default()).unwrap();
let b = baselines(&doc);
assert_eq!(b.len(), 2, "{b:?}");
assert!((gap_after_first(&b) - 15.0).abs() < 0.5, "the row step alone: {b:?}");
let mut doc = text_page("BT /F1 10 Tf 72 700 Td (The quick brown fox) Tj ET");
text::rewrite_block(&mut doc, 0, 0, Some("First one\nSecond one"), &text::BlockStyle { paragraph_spacing: Some(20.0), ..Default::default() })
.unwrap();
assert!((gap_after_first(&baselines(&doc)) - 35.0).abs() < 0.5, "{:?}", baselines(&doc));
let mut doc = text_page("BT /F1 10 Tf 72 700 Td (The quick brown fox) Tj ET");
text::rewrite_block(&mut doc, 0, 0, Some("First one\nSecond one"), &text::BlockStyle { paragraph_spacing: Some(0.0), ..Default::default() })
.unwrap();
assert!((gap_after_first(&baselines(&doc)) - 15.0).abs() < 0.5);
// Without a break, nothing changes: one paragraph, the ordinary line step.
let mut doc = text_page("BT /F1 10 Tf 72 700 Td (The quick brown fox) Tj ET");
text::rewrite_block(
&mut doc,
0,
0,
Some("First one Second one and then some more words to wrap"),
&text::BlockStyle { width: Some(90.0), ..Default::default() },
)
.unwrap();
let b = baselines(&doc);
assert!(b.len() >= 2 && (gap_after_first(&b) - 12.0).abs() < 0.5, "{b:?}");
}
35 changes: 30 additions & 5 deletions crates/edit/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,9 @@ pub struct BlockStyle {
pub underline: Option<bool>,
/// Line spacing as a multiple of the font size (1.2 is ordinary).
pub line_spacing: Option<f64>,
/// Space between paragraphs (a line break in the text starts one), in points, beyond the
/// line spacing; `None` is half the font size when the text has several paragraphs.
pub paragraph_spacing: Option<f64>,
/// Character spacing (points) and horizontal scaling (percent).
pub char_spacing: Option<f64>,
pub scale: Option<f64>,
Expand Down Expand Up @@ -2063,6 +2066,7 @@ fn rewrite_block_from(
let pieces = Pieces::join(&streams.iter().map(|(_, d)| d.as_slice()).collect::<Vec<_>>());
let ops = pieces.parse().ops;
// Explicit line breaks stay (#1443); within a line, runs of white space become one space.
// A line break in the text also starts a paragraph, with the paragraph spacing before it.
let text = text
.unwrap_or(&b.text)
.split('\n')
Expand Down Expand Up @@ -2168,7 +2172,15 @@ fn rewrite_block_from(
width
};
let right_edge = if anchored_right { right_edge } else { width };
let wrapped = wrap(&text, width + 0.5, advance);
// Every paragraph wrapped on its own; `starts` marks a paragraph's first line.
let paragraphs: Vec<&str> = text.split('\n').collect();
let (mut wrapped, mut starts): (Vec<String>, Vec<bool>) = (Vec::new(), Vec::new());
for (pi, p) in paragraphs.iter().enumerate() {
for (li, line) in wrap(p, width + 0.5, advance).into_iter().enumerate() {
wrapped.push(line);
starts.push(pi > 0 && li == 0);
}
}
let mut substituted = None;
let new_font = !reuse;
let (show_font, encode): (String, Encoder) = if let Some(m) = metrics.clone().filter(|_| reuse) {
Expand Down Expand Up @@ -2214,6 +2226,14 @@ fn rewrite_block_from(
None if o_state.leading > 0.0 => o_state.leading,
None => size * 1.2,
};
// The gap before a paragraph, in text space, beyond the line step.
let gap = match style.paragraph_spacing {
Some(p) if p.is_finite() => (p / k).clamp(0.0, 14_400.0),
Some(_) => return Err(EditError::Invalid("the paragraph spacing must be a number".into())),
// Turned text keeps its rows where they were (#1443) unless a spacing is asked for.
None if upright && paragraphs.iter().filter(|p| !p.is_empty()).count() > 1 => size * 0.5,
None => 0.0,
};
let n = pdfcraft_content::num;
// In its own graphics state, so a new colour (or anything else) stops at the paragraph.
let mut block_ops = vec![Op::new("q", vec![])];
Expand Down Expand Up @@ -2256,18 +2276,23 @@ fn rewrite_block_from(
None => type3.is_none(),
};
let mut x = 0.0;
let mut y = 0.0;
let mut tw_set = 0.0;
let mut underlines: Vec<(f64, f64, f64)> = Vec::new(); // (x0, x1, y) in text space
for (i, line) in wrapped.iter().enumerate() {
let spaces = line.matches(' ').count();
let tw =
if justify && i + 1 < wrapped.len() && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 };
// Justify every line but a paragraph's last.
let last_of_paragraph = i + 1 >= wrapped.len() || starts.get(i + 1).copied().unwrap_or(true);
let tw = if justify && !last_of_paragraph && spaces > 0 { (width - advance(line)).max(0.0) / k / o_state.scale / spaces as f64 } else { 0.0 };
// A stretched line fills the width from the left edge.
let dx = if tw > 0.0 { 0.0 } else { offset(line) };
// The line step, plus the gap before a paragraph's first line.
let step = if i > 0 { lead + if starts.get(i).copied().unwrap_or(false) { gap } else { 0.0 } } else { 0.0 };
if i > 0 || dx != 0.0 {
block_ops.push(Op::new("Td", vec![n(dx - x), n(if i > 0 { -lead } else { 0.0 })]));
block_ops.push(Op::new("Td", vec![n(dx - x), n(-step)]));
}
x = dx;
y -= step;
let stretch = if word_spacing_reaches_spaces { 0.0 } else { tw };
let tw = tw - stretch;
if tw != tw_set {
Expand Down Expand Up @@ -2297,7 +2322,7 @@ fn rewrite_block_from(
}
let tw = tw + stretch;
let w = (advance(line) + tw * spaces as f64 * o_state.scale * k) / k;
underlines.push((dx, dx + w, -(i as f64) * lead - size * 0.12));
underlines.push((dx, dx + w, y - size * 0.12));
}
block_ops.push(Op::new("ET", vec![]));
if style.underline == Some(true) {
Expand Down
Loading
Loading