From 09e0464e875fccecd6f2f686d462ad2c4a3b5ecd Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 15 Jul 2024 20:01:51 +0200 Subject: [PATCH 01/15] Fix duplicate completions (#4563) --- crates/typst-ide/src/complete.rs | 27 +++++++++---------------- crates/typst/src/foundations/methods.rs | 21 +------------------ crates/typst/src/foundations/mod.rs | 2 +- 3 files changed, 12 insertions(+), 38 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index f6c96d001..d534f55cc 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -5,8 +5,8 @@ use ecow::{eco_format, EcoString}; use if_chain::if_chain; use serde::{Deserialize, Serialize}; use typst::foundations::{ - fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, - NoneValue, Repr, Scope, StyleChain, Styles, Type, Value, + fields_on, format_str, repr, AutoValue, CastInfo, Func, Label, NoneValue, Repr, + Scope, StyleChain, Styles, Type, Value, }; use typst::model::Document; use typst::syntax::{ @@ -396,19 +396,6 @@ fn field_access_completions( } } - for &(method, args) in mutable_methods_on(value.ty()) { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: method.into(), - apply: Some(if args { - eco_format!("{method}(${{}})") - } else { - eco_format!("{method}()${{}}") - }), - detail: None, - }) - } - for &field in fields_on(value.ty()) { // Complete the field name along with its value. Notes: // 1. No parentheses since function fields cannot currently be called @@ -1394,7 +1381,7 @@ mod tests { } #[test] - fn test_whitespace_in_autocomplete() { + fn test_autocomplete_whitespace() { //Check that extra space before '.' is handled correctly. test("#() .", 5, &[], &["insert", "remove", "len", "all"]); test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]); @@ -1404,10 +1391,16 @@ mod tests { } #[test] - fn test_before_window_char_boundary() { + fn test_autocomplete_before_window_char_boundary() { // Check that the `before_window` doesn't slice into invalid byte // boundaries. let s = "πŸ˜€πŸ˜€ #text(font: \"\")"; test(s, s.len() - 2, &[], &[]); } + + #[test] + fn test_autocomplete_mutable_method() { + let s = "#{ let x = (1, 2, 3); x. }"; + test(s, s.len() - 2, &["at", "push", "pop"], &[]); + } } diff --git a/crates/typst/src/foundations/methods.rs b/crates/typst/src/foundations/methods.rs index 287a49c69..945b7c507 100644 --- a/crates/typst/src/foundations/methods.rs +++ b/crates/typst/src/foundations/methods.rs @@ -1,28 +1,9 @@ //! Handles special built-in methods on values. use crate::diag::{At, SourceResult}; -use crate::foundations::{Args, Array, Dict, Str, Type, Value}; +use crate::foundations::{Args, Str, Type, Value}; use crate::syntax::Span; -/// List the available methods for a type and whether they take arguments. -pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] { - if ty == Type::of::() { - &[ - ("first", false), - ("last", false), - ("at", true), - ("pop", false), - ("push", true), - ("insert", true), - ("remove", true), - ] - } else if ty == Type::of::() { - &[("at", true), ("insert", true), ("remove", true)] - } else { - &[] - } -} - /// Whether a specific method is mutating. pub(crate) fn is_mutating_method(method: &str) -> bool { matches!(method, "push" | "pop" | "insert" | "remove") diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs index b7783dda9..f9dfff4f5 100644 --- a/crates/typst/src/foundations/mod.rs +++ b/crates/typst/src/foundations/mod.rs @@ -49,7 +49,7 @@ pub use self::float::*; pub use self::func::*; pub use self::int::*; pub use self::label::*; -pub use self::methods::*; +pub(crate) use self::methods::*; pub use self::module::*; pub use self::none::*; pub use self::plugin::*; From 993e7a45a9f5e2a18f12aed115d8d9f07557f524 Mon Sep 17 00:00:00 2001 From: Leedehai <18319900+Leedehai@users.noreply.github.com> Date: Wed, 17 Jul 2024 04:19:08 -0400 Subject: [PATCH 02/15] Move the early exit inside `stretch_glyph()` upward (#4570) Co-authored-by: Laurenz --- crates/typst/src/math/stretch.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/typst/src/math/stretch.rs b/crates/typst/src/math/stretch.rs index cc71ced6d..749773e3c 100644 --- a/crates/typst/src/math/stretch.rs +++ b/crates/typst/src/math/stretch.rs @@ -39,7 +39,13 @@ fn stretch_glyph( short_fall: Abs, horizontal: bool, ) -> VariantFragment { + // If the base glyph is good enough, use it. + let advance = if horizontal { base.width } else { base.height() }; let short_target = target - short_fall; + if short_target <= advance { + return base.into_variant(); + } + let mut min_overlap = Abs::zero(); let construction = ctx .table @@ -55,12 +61,6 @@ fn stretch_glyph( }) .unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) }); - // If the base glyph is good enough, use it. - let advance = if horizontal { base.width } else { base.height() }; - if short_target <= advance { - return base.into_variant(); - } - // Search for a pre-made variant with a good advance. let mut best_id = base.id; let mut best_advance = base.width; From df56a2d20ddae5f4ea9aa39a4a1a8b7b11cf7c85 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 17 Jul 2024 04:27:46 -0400 Subject: [PATCH 03/15] Allow absolute lengths in `scale` (#4271) Co-authored-by: Laurenz --- crates/typst/src/layout/ratio.rs | 2 +- crates/typst/src/layout/transform.rs | 99 ++++++++++++++++++--- tests/ref/transform-scale-abs-and-auto.png | Bin 0 -> 3719 bytes tests/suite/layout/transform.typ | 13 ++- 4 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 tests/ref/transform-scale-abs-and-auto.png diff --git a/crates/typst/src/layout/ratio.rs b/crates/typst/src/layout/ratio.rs index 2d791f2d4..020d689aa 100644 --- a/crates/typst/src/layout/ratio.rs +++ b/crates/typst/src/layout/ratio.rs @@ -70,7 +70,7 @@ impl Ratio { impl Debug for Ratio { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{:?}%", self.get()) + write!(f, "{:?}%", self.get() * 100.0) } } diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index 7172466f3..ad3668d10 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -1,7 +1,11 @@ -use crate::diag::SourceResult; +use std::ops::Div; + +use once_cell::unsync::Lazy; + +use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, StyleChain, + cast, elem, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain, }; use crate::introspection::Locator; use crate::layout::{ @@ -188,15 +192,15 @@ pub struct ScaleElem { let all = args.find()?; args.named("x")?.or(all) )] - #[default(Ratio::one())] - pub x: Ratio, + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub x: Smart, /// The vertical scaling factor. /// /// The body will be mirrored vertically if the parameter is negative. #[parse(args.named("y")?.or(all))] - #[default(Ratio::one())] - pub y: Ratio, + #[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))] + pub y: Smart, /// The origin of the transformation. /// @@ -242,12 +246,9 @@ fn layout_scale( styles: StyleChain, region: Region, ) -> SourceResult { - let sx = elem.x(styles); - let sy = elem.y(styles); - let align = elem.origin(styles).resolve(styles); - // Compute the new region's approximate size. - let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs); + let scale = elem.resolve_scale(engine, locator.relayout(), region.size, styles)?; + let size = region.size.zip_map(scale, |r, s| s.of(r)).map(Abs::abs); measure_and_layout( engine, @@ -256,12 +257,84 @@ fn layout_scale( size, styles, elem.body(), - Transform::scale(sx, sy), - align, + Transform::scale(scale.x, scale.y), + elem.origin(styles).resolve(styles), elem.reflow(styles), ) } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum ScaleAmount { + Ratio(Ratio), + Length(Length), +} + +impl Packed { + /// Resolves scale parameters, preserving aspect ratio if one of the scales is set to `auto`. + fn resolve_scale( + &self, + engine: &mut Engine, + locator: Locator, + container: Size, + styles: StyleChain, + ) -> SourceResult> { + fn resolve_axis( + axis: Smart, + body: impl Fn() -> SourceResult, + styles: StyleChain, + ) -> SourceResult> { + Ok(match axis { + Smart::Auto => Smart::Auto, + Smart::Custom(amt) => Smart::Custom(match amt { + ScaleAmount::Ratio(ratio) => ratio, + ScaleAmount::Length(length) => { + let length = length.resolve(styles); + Ratio::new(length.div(body()?)) + } + }), + }) + } + + let size = Lazy::new(|| { + let pod = Regions::one(container, Axes::splat(false)); + let frame = self.body().layout(engine, locator, styles, pod)?.into_frame(); + SourceResult::Ok(frame.size()) + }); + + let x = resolve_axis( + self.x(styles), + || size.as_ref().map(|size| size.x).map_err(Clone::clone), + styles, + )?; + + let y = resolve_axis( + self.y(styles), + || size.as_ref().map(|size| size.y).map_err(Clone::clone), + styles, + )?; + + match (x, y) { + (Smart::Auto, Smart::Auto) => { + bail!(self.span(), "x and y cannot both be auto") + } + (Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)), + (Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => { + Ok(Axes::splat(v)) + } + } + } +} + +cast! { + ScaleAmount, + self => match self { + ScaleAmount::Ratio(ratio) => ratio.into_value(), + ScaleAmount::Length(length) => length.into_value(), + }, + ratio: Ratio => ScaleAmount::Ratio(ratio), + length: Length => ScaleAmount::Length(length), +} + /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { diff --git a/tests/ref/transform-scale-abs-and-auto.png b/tests/ref/transform-scale-abs-and-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..9b50396d62149a3f5d6472736b74f914dc302078 GIT binary patch literal 3719 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 literal 0 HcmV?d00001 diff --git a/tests/suite/layout/transform.typ b/tests/suite/layout/transform.typ index 50a6d417f..3604b72f7 100644 --- a/tests/suite/layout/transform.typ +++ b/tests/suite/layout/transform.typ @@ -74,7 +74,7 @@ Hello #rotated[World]!\ Hello #rotated[World]! --- transform-scale --- -// Test that scaling impact layout. +// Test that scaling impacts layout. #set page(width: 200pt) #set text(size: 32pt) #let scaled(body) = box(scale( @@ -104,3 +104,14 @@ Hello #scaled[World]!\ #set scale(reflow: true) Hello #scaled[World]! + +--- transform-scale-abs-and-auto --- +// Test scaling by absolute lengths and auto. +#set page(width: 200pt, height: 200pt) +#let cylinder = image("/assets/images/cylinder.svg") + +#cylinder +#scale(x: 100pt, y: 50pt, reflow: true, cylinder) +#scale(x: auto, y: 50pt, reflow: true, cylinder) +#scale(x: 100pt, y: auto, reflow: true, cylinder) +#scale(x: 150%, y: auto, reflow: true, cylinder) From 0ea4b1b217b2b5b902dca8e6e6ce11e1890afd40 Mon Sep 17 00:00:00 2001 From: mkorje Date: Wed, 17 Jul 2024 16:24:02 +0000 Subject: [PATCH 04/15] Update nixpkgs input (#4575) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index cdc4e5deb..594b8138b 100644 --- a/flake.lock +++ b/flake.lock @@ -39,11 +39,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1706371002, - "narHash": "sha256-dwuorKimqSYgyu8Cw6ncKhyQjUDOyuXoxDTVmAXq88s=", + "lastModified": 1720957393, + "narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c002c6aa977ad22c60398daaa9be52f2203d0006", + "rev": "693bc46d169f5af9c992095736e82c3488bf7dbb", "type": "github" }, "original": { From 42754477886f6a12afbabfd2a64d8c787a57bc03 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 18 Jul 2024 10:49:08 +0200 Subject: [PATCH 05/15] Fix panic in link linebreaking (#4579) --- crates/typst/src/layout/inline/linebreak.rs | 12 +++++------- tests/ref/issue-hyphenate-in-link.png | Bin 0 -> 1200 bytes tests/suite/layout/inline/linebreak.typ | 8 ++++++++ 3 files changed, 13 insertions(+), 7 deletions(-) create mode 100644 tests/ref/issue-hyphenate-in-link.png diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index 075d24b33..1f30bb732 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -655,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { let (head, tail) = text.split_at(last); if head.ends_with("://") || tail.starts_with("www.") { let (link, _) = link_prefix(tail); - let end = last + link.len(); linebreak_link(link, |i| f(last + i, Breakpoint::Normal)); - while iter.peek().is_some_and(|&p| p < end) { + last += link.len(); + while iter.peek().is_some_and(|&p| p < last) { iter.next(); } } @@ -687,19 +687,17 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { }; // Hyphenate between the last and current breakpoint. - if hyphenate { - let mut offset = last; + if hyphenate && last < point { for segment in text[last..point].split_word_bounds() { if !segment.is_empty() && segment.chars().all(char::is_alphabetic) { - hyphenations(p, &lb, offset, segment, &mut f); + hyphenations(p, &lb, last, segment, &mut f); } - offset += segment.len(); + last += segment.len(); } } // Call `f` for the UAX #14 break opportunity. f(point, breakpoint); - last = point; } } diff --git a/tests/ref/issue-hyphenate-in-link.png b/tests/ref/issue-hyphenate-in-link.png new file mode 100644 index 0000000000000000000000000000000000000000..932c23ae994d61febff645f091481cdf34f4fbfb GIT binary patch literal 1200 zcmV;h1W)^kP) z*Mh8bZ=chzE8CMp?PKoiapYqV9W|$=Zqe1Ll1BTAL}sht+>8-BHqlC7h6cg0aUSZl;wW9IcrV+I|h3r$Fd85VAMEv$-kt^dQ zYsQXh*m<*c*@PL|6WG(t`_uU!yU7)NL6vXJ>9_5-t!^v0p)c zbhpL5U>FV-quqvpvLi&*TZ53w2f$WSzzJq0<%8#TT}c z``U-tRq$q$tD|}=BXfJaJzJY6zrvgBvQ)f+3)H&NT5hzN$$Y8>Wr>LX~G%=?1gXX=t=i@q(k~-sx+*#5RWbr*|2Rf2+ z>t-_`K^6dpV92M-WmC^oO@Xv;Mw52d@r7Dn_Q|f{0*~2b50=z>^ zTXC#n=14&M-gNMFsn}tRxO$u-Sw;@mEC`UMVa^csx>PxhhKSmP%b4;Ky?uG20gqHv zJ6UloR3?v{kL=gQKZA+*GEuyj^U6Gc*1Uag{S^sVOBhltp9XFVN3)xN+udbZo>d?L z0s3zUoY2)AxKy>0fI7z#q8jfKRDUW_n_zE1!sH*>9_PYwW2Mt*bcy${*-$PdbHZqJ z?9yow6W>I>gLLRyWs|ojAcLNnZhleBEVxh+1bNF-~t8GZ|IUHNg zrib2K-^0Qy1Y2&Kd=K9i5$u)Vdl>n-TCenFz`$sqf4TgyANIrld-xCNHj=Q(`<8|P O0000 Date: Fri, 19 Jul 2024 13:47:51 +0200 Subject: [PATCH 06/15] Tune hyphenation (#4584) --- crates/typst/src/layout/inline/line.rs | 2 +- crates/typst/src/layout/inline/linebreak.rs | 135 +++++++++++--------- crates/typst/src/text/mod.rs | 5 - tests/ref/justify-avoid-runts.png | Bin 1879 -> 1885 bytes tests/ref/justify-chinese.png | Bin 6678 -> 6583 bytes 5 files changed, 74 insertions(+), 68 deletions(-) diff --git a/crates/typst/src/layout/inline/line.rs b/crates/typst/src/layout/inline/line.rs index 12162ab16..bf1662ce3 100644 --- a/crates/typst/src/layout/inline/line.rs +++ b/crates/typst/src/layout/inline/line.rs @@ -133,7 +133,7 @@ pub fn line<'a>( || (p.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. - let dash = if breakpoint == Breakpoint::Hyphen || full.ends_with(SHY) { + let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { Some(Dash::Soft) } else if full.ends_with(HYPHEN) { Some(Dash::Hard) diff --git a/crates/typst/src/layout/inline/linebreak.rs b/crates/typst/src/layout/inline/linebreak.rs index 1f30bb732..defb5f81c 100644 --- a/crates/typst/src/layout/inline/linebreak.rs +++ b/crates/typst/src/layout/inline/linebreak.rs @@ -1,5 +1,6 @@ use std::ops::{Add, Sub}; +use az::SaturatingAs; use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed}; use icu_properties::sets::CodePointSetData; use icu_properties::LineBreak; @@ -21,10 +22,15 @@ use crate::text::{Lang, TextElem}; type Cost = f64; // Cost parameters. -const DEFAULT_HYPH_COST: Cost = 0.5; -const DEFAULT_RUNT_COST: Cost = 0.5; -const CONSECUTIVE_DASH_COST: Cost = 0.3; -const MAX_COST: Cost = 1_000_000.0; +// +// We choose higher costs than the Knuth-Plass paper (which would be 50) because +// it hyphenates way to eagerly in Typst otherwise. Could be related to the +// ratios coming out differently since Typst doesn't have the concept of glue, +// so things work a bit differently. +const DEFAULT_HYPH_COST: Cost = 135.0; +const DEFAULT_RUNT_COST: Cost = 100.0; + +// Other parameters. const MIN_RATIO: f64 = -1.0; const MIN_APPROX_RATIO: f64 = -0.5; const BOUND_EPS: f64 = 1e-3; @@ -65,8 +71,9 @@ pub enum Breakpoint { Normal, /// A mandatory breakpoint (after '\n' or at the end of the text). Mandatory, - /// An opportunity for hyphenating. - Hyphen, + /// An opportunity for hyphenating and how many chars are before/after it + /// in the word. + Hyphen(u8, u8), } impl Breakpoint { @@ -95,9 +102,14 @@ impl Breakpoint { } // Trim nothing further. - Self::Hyphen => line, + Self::Hyphen(..) => line, } } + + /// Whether this is a hyphen breakpoint. + pub fn is_hyphen(self) -> bool { + matches!(self, Self::Hyphen(..)) + } } /// Breaks the paragraph into lines. @@ -254,7 +266,6 @@ fn linebreak_optimized_bounded<'a>( width, &pred.line, &attempt, - end, breakpoint, unbreakable, ); @@ -374,8 +385,6 @@ fn linebreak_optimized_approximate( let mut prev_end = 0; breakpoints(p, |end, breakpoint| { - let at_end = end == p.text.len(); - // Find the optimal predecessor. let mut best: Option = None; for (pred_index, pred) in table.iter().enumerate().skip(active) { @@ -384,13 +393,12 @@ fn linebreak_optimized_approximate( // Whether the line is justified. This is not 100% accurate w.r.t // to line()'s behaviour, but good enough. - let justify = p.justify && !at_end && breakpoint != Breakpoint::Mandatory; + let justify = p.justify && breakpoint != Breakpoint::Mandatory; // We don't really know whether the line naturally ends with a dash // here, so we can miss that case, but it's ok, since all of this // just an estimate. - let consecutive_dash = - pred.breakpoint == Breakpoint::Hyphen && breakpoint == Breakpoint::Hyphen; + let consecutive_dash = pred.breakpoint.is_hyphen() && breakpoint.is_hyphen(); // Estimate how much the line's spaces would need to be stretched to // make it the desired width. We trim at the end to not take into @@ -401,7 +409,7 @@ fn linebreak_optimized_approximate( p, width, estimates.widths.estimate(start..trimmed_end) - + if breakpoint == Breakpoint::Hyphen { + + if breakpoint.is_hyphen() { metrics.approx_hyphen_width } else { Abs::zero() @@ -416,7 +424,6 @@ fn linebreak_optimized_approximate( metrics, breakpoint, line_ratio, - at_end, justify, unbreakable, consecutive_dash, @@ -474,17 +481,8 @@ fn linebreak_optimized_approximate( let Entry { end, breakpoint, unbreakable, .. } = table[idx]; let attempt = line(engine, p, start..end, breakpoint, Some(&pred)); - - let (_, line_cost) = ratio_and_cost( - p, - metrics, - width, - &pred, - &attempt, - end, - breakpoint, - unbreakable, - ); + let (_, line_cost) = + ratio_and_cost(p, metrics, width, &pred, &attempt, breakpoint, unbreakable); pred = attempt; start = end; @@ -502,7 +500,6 @@ fn ratio_and_cost( available_width: Abs, pred: &Line, attempt: &Line, - end: usize, breakpoint: Breakpoint, unbreakable: bool, ) -> (f64, Cost) { @@ -519,7 +516,6 @@ fn ratio_and_cost( metrics, breakpoint, ratio, - end == p.text.len(), attempt.justify, unbreakable, pred.dash.is_some() && attempt.dash.is_some(), @@ -569,57 +565,64 @@ fn raw_ratio( } /// Compute the cost of a line given raw metrics. -#[allow(clippy::too_many_arguments)] +/// +/// This mostly follows the formula in the Knuth-Plass paper, but there are some +/// adjustments. fn raw_cost( metrics: &CostMetrics, breakpoint: Breakpoint, ratio: f64, - at_end: bool, justify: bool, unbreakable: bool, consecutive_dash: bool, approx: bool, ) -> Cost { - // Determine the cost of the line. - let mut cost = if ratio < metrics.min_ratio(approx) { + // Determine the stretch/shrink cost of the line. + let badness = if ratio < metrics.min_ratio(approx) { // Overfull line always has maximum cost. - MAX_COST - } else if breakpoint == Breakpoint::Mandatory || at_end { - // - If ratio < 0, we always need to shrink the line (even the last one). - // - If ratio > 0, we need to stretch the line only when it is justified - // (last line is not justified by default even if `p.justify` is true). - if ratio < 0.0 || (ratio > 0.0 && justify) { - ratio.powi(3).abs() - } else { - 0.0 - } + 1_000_000.0 + } else if justify || ratio < 0.0 { + // If the line shall be justified or needs shrinking, it has normal + // badness with cost 100|ratio|^3. We limit the ratio to 10 as to not + // get to close to our maximum cost. + 100.0 * ratio.abs().min(10.0).powi(3) } else { - // Normal line with cost of |ratio^3|. - ratio.powi(3).abs() + // If the line shouldn't be justified and doesn't need shrink, we don't + // pay any cost. + 0.0 }; - // Penalize runts (lone words in the last line). - if unbreakable && at_end { - cost += metrics.runt_cost; + // Compute penalties. + let mut penalty = 0.0; + + // Penalize runts (lone words before a mandatory break / at the end). + if unbreakable && breakpoint == Breakpoint::Mandatory { + penalty += metrics.runt_cost; } // Penalize hyphenation. - if breakpoint == Breakpoint::Hyphen { - cost += metrics.hyph_cost; + if let Breakpoint::Hyphen(l, r) = breakpoint { + // We penalize hyphenations close to the edges of the word (< LIMIT + // chars) extra. For each step of distance from the limit, we add 15% + // to the cost. + const LIMIT: u8 = 5; + let steps = LIMIT.saturating_sub(l) + LIMIT.saturating_sub(r); + let extra = 0.15 * steps as f64; + penalty += (1.0 + extra) * metrics.hyph_cost; } - // In the Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a, - // where r is the ratio, p=50 is the penalty, and a=3000 is - // consecutive the penalty. We divide the whole formula by 10, - // resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3 - let mut cost = (0.01 + cost).powi(2); - - // Penalize two consecutive dashes (not necessarily hyphens) extra. + // Penalize two consecutive dashes extra (not necessarily hyphens). + // Knuth-Plass does this separately after the squaring, with a higher cost, + // but I couldn't find any explanation as to why. if consecutive_dash { - cost += CONSECUTIVE_DASH_COST; + penalty += metrics.hyph_cost; } - cost + // From the Knuth-Plass Paper: $ (1 + beta_j + pi_j)^2 $. + // + // We add one to minimize the number of lines when everything else is more + // or less equal. + (1.0 + badness + penalty).powi(2) } /// Calls `f` for all possible points in the text where lines can broken. @@ -711,10 +714,13 @@ fn hyphenations( mut f: impl FnMut(usize, Breakpoint), ) { let Some(lang) = lang_at(p, offset) else { return }; + let count = word.chars().count(); let end = offset + word.len(); + let mut chars = 0; for syllable in hypher::hyphenate(word, lang) { offset += syllable.len(); + chars += syllable.chars().count(); // Don't hyphenate after the final syllable. if offset == end { @@ -735,8 +741,12 @@ fn hyphenations( continue; } + // Determine the number of codepoints before and after the hyphenation. + let l = chars.saturating_as::(); + let r = (count - chars).saturating_as::(); + // Call `f` for the word-internal hyphenation opportunity. - f(offset, Breakpoint::Hyphen); + f(offset, Breakpoint::Hyphen(l, r)); } } @@ -825,9 +835,9 @@ fn lang_at(p: &Preparation, offset: usize) -> Option { struct CostMetrics { min_ratio: f64, min_approx_ratio: f64, + approx_hyphen_width: Abs, hyph_cost: Cost, runt_cost: Cost, - approx_hyphen_width: Abs, } impl CostMetrics { @@ -837,10 +847,11 @@ impl CostMetrics { // When justifying, we may stretch spaces below their natural width. min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), // Approximate hyphen width for estimates. approx_hyphen_width: Em::new(0.33).at(p.size), + // Costs. + hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), } } diff --git a/crates/typst/src/text/mod.rs b/crates/typst/src/text/mod.rs index d42e4df8b..76ea26c10 100644 --- a/crates/typst/src/text/mod.rs +++ b/crates/typst/src/text/mod.rs @@ -512,11 +512,6 @@ pub struct TextElem { /// default of `auto`, prevents them. More nuanced cost specification for /// these modifications is planned for the future.) /// - /// The default costs are an acceptable balance, but some may find that it - /// hyphenates or avoids runs too eagerly, breaking the flow of dense prose. - /// A cost of 600% (six times the normal cost) may work better for such - /// contexts. - /// /// ```example /// #set text(hyphenate: true, size: 11.4pt) /// #set par(justify: true) diff --git a/tests/ref/justify-avoid-runts.png b/tests/ref/justify-avoid-runts.png index 70513939cd3945839038297afe1b4f4ae78eb5b7..a0c84eec515b2b967baeebea86b06fbbe4ae2038 100644 GIT binary patch delta 1870 zcmV-U2eJ6q4&4rrB!47HL_t(|+U=L?Q`1)-#`|yV?9Oam*WGpMxJ)l<>)N_log%aj z^@b~5cXh=T)FKzr3I#;DnSk6P5Rw>@2#6(s2%1UJd-!@8qXVExmWI${|4oq27k`A(MnEeyJ6&{Z9j0r z!P@!BtAjY#OiOw49SxjR9?l7$zBNC2-)?ZuYF-%g9VawIa=-ry`HU{Ri^F2O|YcNt`eH7&kFLm_$o%FDh+_SRQim`{c}?1hr#1R9S) z<0cH;N@Vgwh>`==b@=`XFs{c20Pw-B&j`DEC#t#fu^hF(SbOae;N?m&Rq9r_gxXwx zC9Hi4fIP}Qm16CI4}sKdAsouh-Y3?sOa>lp6hhbeQhy(@cJup$zsvw!d0c&;ACm{+ zt6Ri3A36qrdxu;rS&8rAP81WhPjWOK-~sguN&Ck-uz!C566K{VCMHj z79()ww0#8tybHaAuvLW~uKeMd`sf~DBuR5!_!ug40<*E@k{SRR1sW$|>O|60f?Tdk zbpZMtg?|a`V{{{cJ85;to&jBHS_2Ee4^yx@<0?-z5l8bz^63c#MqoKaTD6B z^9A{Gb@C(;SCn}S&H9Ui{G);_J6C@9UiT3}esA_oD{;NGqmfYctnQp39~)jyaD0e{ zPGZq)UKIg-E5BQwO>ZEq23|L>VbE)?Rya&GaBa_dCqbF5ik=`I zE0Eg>(1fTN(&RY_4!{wrDh4~!!<7uj)hXG8!AQ+z?+fRZFFN`7b53aQ=RTS)PWa*_ zZ=IgaOYYt$<)jtsIbq)t&J0>V*Kw}Og61X1#Vk%Z{qejfC!_2a4GZ+DYiv@lD1WR> z9-7%mKvzK_L!Gjp$=_4U?ToT~6XA#Q20>n)Zer9a4FHC#)h;GKA!#9?MWF`Zs!^Dj zd{d#$#g*5bK?0%3tSV>nc-Mv&U_1bFBhalk|C>qQbxUH2fukK2gcJn0n0%<)hft)L zJ42AnrQBw6neCk)fv3A5u>gjq&VOho-;AhSuKZAq0JG|3qF8(41)%3>1Z`WhW4kXt;_kS1VDVMo& z%f6WFg!565m$6$_&CsBoVDir{T<6QbJ&ACFY$U&m$s=d~@qlyz zb7zNv$6Dk6n0%!*E}0nIf477X3x7fW_}=@3(kxpH;j}HWg~`)@@_!iuOxxWjEx_&T z_hXrSgZ+S#D?f5ofJOJ!DbKK|fE%8FBDJM!Jj3!iaj6(~YSx&<+LsV^2sL0_6ISRM zmO4O_AcQWx;#1GCgmUFcUt@|8G*atXwcTC7gSX+D1#|`Q$aQCle;i_0pKnK?xM~yY z31Ko8!f!@d;29PZP=D_Sn*)$mnLVo})o|q#ficyDl=RHqX5gFf;u5fGgJpA6+kuje z?ZD)xYN-Q2-A3sEFtbe;Tm<0Yxr~0o@aDLigs8mJA=5z7afL9^q5IjDs-xOXJwV@1 zO_ukC^U4>S-1!>i5ize(ej{$70x_YETxfaAQRiMpSVw~=-XFPvBIm3!r0`L8%X15QX#v~Nxz>;M1&07*qo IM6N<$g1>~Bt^fc4 delta 1864 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 Date: Sat, 20 Jul 2024 14:26:57 +0200 Subject: [PATCH 07/15] Fix approximated size of `reflow: true` transformations (#4462) Co-authored-by: Laurenz --- crates/typst/src/layout/transform.rs | 33 ++++++++++-------- tests/ref/transform-scale-relative-sizing.png | Bin 1975 -> 2033 bytes tests/ref/transform-scale.png | Bin 1802 -> 1804 bytes 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/crates/typst/src/layout/transform.rs b/crates/typst/src/layout/transform.rs index ad3668d10..dcb178fdb 100644 --- a/crates/typst/src/layout/transform.rs +++ b/crates/typst/src/layout/transform.rs @@ -12,6 +12,7 @@ use crate::layout::{ Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length, Point, Ratio, Region, Regions, Rel, Size, VAlignment, }; +use crate::utils::Numeric; /// Moves content without affecting layout. /// @@ -153,12 +154,11 @@ fn layout_rotate( let align = elem.origin(styles).resolve(styles); // Compute the new region's approximate size. - let size = region - .size - .to_point() - .transform_inf(Transform::rotate(angle)) - .map(Abs::abs) - .to_size(); + let size = if region.size.is_finite() { + compute_bounding_box(region.size, Transform::rotate(-angle)).1 + } else { + Size::splat(Abs::inf()) + }; measure_and_layout( engine, @@ -248,7 +248,10 @@ fn layout_scale( ) -> SourceResult { // Compute the new region's approximate size. let scale = elem.resolve_scale(engine, locator.relayout(), region.size, styles)?; - let size = region.size.zip_map(scale, |r, s| s.of(r)).map(Abs::abs); + let size = region + .size + .zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r }) + .map(Abs::abs); measure_and_layout( engine, @@ -489,7 +492,7 @@ fn measure_and_layout( .pre_concat(Transform::translate(-x, -y)); // Compute the bounding box and offset and wrap in a new frame. - let (offset, size) = compute_bounding_box(&frame, ts); + let (offset, size) = compute_bounding_box(frame.size(), ts); frame.transform(ts); frame.translate(offset); frame.set_size(size); @@ -512,20 +515,20 @@ fn measure_and_layout( } } -/// Computes the bounding box and offset of a transformed frame. -fn compute_bounding_box(frame: &Frame, ts: Transform) -> (Point, Size) { +/// Computes the bounding box and offset of a transformed area. +fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) { let top_left = Point::zero().transform_inf(ts); - let top_right = Point::new(frame.width(), Abs::zero()).transform_inf(ts); - let bottom_left = Point::new(Abs::zero(), frame.height()).transform_inf(ts); - let bottom_right = Point::new(frame.width(), frame.height()).transform_inf(ts); + let top_right = Point::with_x(size.x).transform_inf(ts); + let bottom_left = Point::with_y(size.y).transform_inf(ts); + let bottom_right = size.to_point().transform_inf(ts); - // We first compute the new bounding box of the rotated frame. + // We first compute the new bounding box of the rotated area. let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x); let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y); let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x); let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y); - // Then we compute the new size of the frame. + // Then we compute the new size of the area. let width = max_x - min_x; let height = max_y - min_y; diff --git a/tests/ref/transform-scale-relative-sizing.png b/tests/ref/transform-scale-relative-sizing.png index d10bd3ff4f364915a2884bd2991479fc1b9b9d45..a6c18a98e9c6a9ed3747ca9d1ec150de126d3208 100644 GIT binary patch delta 1882 zcmV-g2c`J85AhF>B!9R`L_t(|+U?tYa1`|&$MO68NC@F6VNj7GMW|FBWP%{_R81-~ zj+S7Oi3pT|@L)oC7*ZfWcqmsowiN9MwYHFn8lc)%7>GbYQ;{P<6io5T0LmfB1qjAG zn2RJBB)NRty~`yza>-=o_B1-*_dh!~do#DM`Tb`1c4vPk{(nd^2}B?Q5r{y3Fc8~K zx~eMp0Yh?DJu^`4)ewJp_K}cBIwa)ayakJvEMAzHNWBg7lNT>poIHOUy~~adRq6;? zv*3PU3XQLJ@*VsAgPdMJ5kPp-R_eW;(htC})!Dqg%}O1oR#ga|rt$3uM)*Coeh$1& zPgbC-gDu;m)PG6p-Gdo4zP|&#{f0aZoTDda5Qfr6hm<-+y}<#2>;eY(4S5zQVq@VL z_dP(40%QG#Bm&piSTBq>1ah;g?QXDng}|(i330xcl$7*BUS5*c8+m#Awaxat=Dl0V zX4QAabAf{#TNp7s_OErVLGoq~>-n=M_SDl?b-7kk0)Mpd7O)Y{gMfq{GsbPiT< zb=U{wenit^-aSUbruUBj3#pMQ`y9|#a+53sA?(r30$&!0V4*Ai0O z8T06FDUjU?#1Uq)lFW3UzqaTf9QS7GkgVGw?@kC1ft(vfzk=%_~R_5M(c4ZUf>BRb%LGt5T=gvYN`ayG~yj z4}UoW3~2+x2UX{Cx>}*SUhf!ph_iuwHrCaBI=a~xILxN98tz9pz&)q_tW-K(MXQ>h(YrwQ7f|zD3kJ8`RTAzxyWkr+fT_+fyKe|IbbXju@?fw@1FAEs% zALPYOaMhc7zkn+>pQQ8meW3N=L6XyytEI<-lwOw_lApx|hm0B)^h6;Is3=q=`3I@K zR$5W%C@=n;dZoo>6_pibpF7;+{l9&OFHx_e_)CZOrOI2^^!=qpboE8f?o(AXrUlX* zc|a$VZU-cP`A2UAvbNJzzn{K6}j?H6s-E3_3{zH&MLLNUGjtb%I{-LVD-C^dyi&(y>daI2zxlL29ah5b}5^Fw;SI zHR}QFtYeG*0enck-f9|+?-ix)_7}n@fs^!91@xzXj}8E*s5eN>pz&oG0Z9M~*jP7= zrjHJJAcF$}*{9S{*VP!*yj^+`p$v*Wc>=L z%`~8YnX145h<9#CeprukYR$!-s|lQ!9GYbTA3+hw!amP{;TJ3BZ!K#*5} zy=`1a%78g^w+?uV4SjSYBHTgu;tIVX?H-a7lL7>J6?mx)NTq`3m;VABYl2uzW@<&c zJmy91hScT-2yzUV+6ILE@e{h+2oy9y+ywgo-Q$S(u!Kg}_BzC?E!H6xD}ssDI? z{vzL}rc-MwkV{wdF@nt?t|5uCBxoLU=K1dw5)YiECub)Nq26?0I$a$vC;3m{ztroa zenqVdo;ABS`y;(ML?4nZDYTo+_87Jwa^hu&zmWPtz-vv10)&=PtDqZ@SVi7iJNyfh z>wzuQDhW~psh0-4K&`ciqZh=PC>K&y(2n!WsNt4F(wO1yD5h4 zhh!@?#y?1QN^08LHLI2{r{3meE7R7dt^CanYTZbGW?WQKHnq~1tzNTc^|F5`T};vU zr!KukE!(Qe55;fzn9Z`jm*Z7-728++`pF;o0WrM#+mi=$lQ9Qw90C!DKsrF*0#v+r UAh4YlNdN!<07*qoM6N<$f(CDo?f?J) delta 1861 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 Date: Sat, 20 Jul 2024 14:51:24 +0200 Subject: [PATCH 08/15] Support transparent page fill (#4586) Co-authored-by: Martin Haug --- crates/typst-cli/src/compile.rs | 11 +++--- crates/typst-pdf/src/color_font.rs | 2 +- crates/typst-pdf/src/content.rs | 6 ++++ crates/typst-pdf/src/page.rs | 9 +++-- crates/typst-pdf/src/pattern.rs | 2 +- crates/typst-render/src/lib.rs | 46 +++++++++++++----------- crates/typst-svg/src/lib.rs | 24 +++++++++---- crates/typst/src/layout/page.rs | 52 ++++++++++++++++++++++------ docs/src/main.rs | 5 ++- tests/fuzz/src/compile.rs | 3 +- tests/ref/page-fill-none.png | Bin 0 -> 97 bytes tests/ref/pattern-relative-self.png | Bin 423 -> 474 bytes tests/src/run.rs | 9 ++--- tests/suite/layout/page.typ | 8 ++++- tests/suite/visualize/pattern.typ | 16 ++++++--- 15 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 tests/ref/page-fill-none.png diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index d15264255..cc85d9209 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -10,10 +10,9 @@ use parking_lot::RwLock; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned}; use typst::foundations::{Datetime, Smart}; -use typst::layout::{Frame, PageRanges}; +use typst::layout::{Frame, Page, PageRanges}; use typst::model::Document; use typst::syntax::{FileId, Source, Span}; -use typst::visualize::Color; use typst::WorldExt; use crate::args::{ @@ -269,7 +268,7 @@ fn export_image( Output::Stdout => Output::Stdout, }; - export_image_page(command, &page.frame, &output, fmt)?; + export_image_page(command, page, &output, fmt)?; Ok(()) }) .collect::, EcoString>>()?; @@ -309,13 +308,13 @@ mod output_template { /// Export single image. fn export_image_page( command: &CompileCommand, - frame: &Frame, + page: &Page, output: &Output, fmt: ImageExportFormat, ) -> StrResult<()> { match fmt { ImageExportFormat::Png => { - let pixmap = typst_render::render(frame, command.ppi / 72.0, Color::WHITE); + let pixmap = typst_render::render(page, command.ppi / 72.0); let buf = pixmap .encode_png() .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; @@ -324,7 +323,7 @@ fn export_image_page( .map_err(|err| eco_format!("failed to write PNG file ({err})"))?; } ImageExportFormat::Svg => { - let svg = typst_svg::svg(frame); + let svg = typst_svg::svg(page); output .write(svg.as_bytes()) .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index 201915b19..4889d9151 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -243,7 +243,7 @@ impl ColorFontMap<()> { let width = font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); let instructions = - content::build(&mut self.resources, &frame, Some(width as f32)); + content::build(&mut self.resources, &frame, None, Some(width as f32)); color_font.glyphs.push(ColorGlyph { gid, instructions }); color_font.glyph_indices.insert(gid, index); diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index da9e4ed44..d9830e439 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -36,6 +36,7 @@ use crate::{deflate_deferred, AbsExt, EmExt}; pub fn build( resources: &mut Resources<()>, frame: &Frame, + fill: Option, color_glyph_width: Option, ) -> Encoded { let size = frame.size(); @@ -53,6 +54,11 @@ pub fn build( .post_concat(Transform::translate(Abs::zero(), size.y)), ); + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + write_shape(&mut ctx, Point::zero(), &shape); + } + // Encode the frame into the content stream. write_frame(&mut ctx, frame); diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 2983f504f..b07490cc0 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -8,7 +8,7 @@ use pdf_writer::{ }; use typst::foundations::Label; use typst::introspection::Location; -use typst::layout::{Abs, Frame}; +use typst::layout::{Abs, Page}; use typst::model::{Destination, Numbering}; use typst::text::Case; @@ -33,7 +33,7 @@ pub fn traverse_pages( pages.push(None); skipped_pages += 1; } else { - let mut encoded = construct_page(&mut resources, &page.frame); + let mut encoded = construct_page(&mut resources, page); encoded.label = page .numbering .as_ref() @@ -60,9 +60,8 @@ pub fn traverse_pages( /// Construct a page object. #[typst_macros::time(name = "construct page")] -fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage { - let content = content::build(out, frame, None); - +fn construct_page(out: &mut Resources<()>, page: &Page) -> EncodedPage { + let content = content::build(out, &page.frame, page.fill_or_transparent(), None); EncodedPage { content, label: None } } diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs index e06c04f87..d4f5a6e08 100644 --- a/crates/typst-pdf/src/pattern.rs +++ b/crates/typst-pdf/src/pattern.rs @@ -103,7 +103,7 @@ fn register_pattern( }; // Render the body. - let content = content::build(&mut patterns.resources, pattern.frame(), None); + let content = content::build(&mut patterns.resources, pattern.frame(), None, None); let pdf_pattern = PdfPattern { transform, diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 305dcd1fc..d5eeacce8 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -7,45 +7,49 @@ mod text; use tiny_skia as sk; use typst::layout::{ - Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Size, Transform, + Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Size, Transform, }; use typst::model::Document; -use typst::visualize::Color; +use typst::visualize::{Color, Geometry, Paint}; -/// Export a frame into a raster image. +/// Export a page into a raster image. /// -/// This renders the frame at the given number of pixels per point and returns +/// This renders the page at the given number of pixels per point and returns /// the resulting `tiny-skia` pixel buffer. #[typst_macros::time(name = "render")] -pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { - let size = frame.size(); +pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap { + let size = page.frame.size(); let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; - let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(fill)); - let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame); + let state = State::new(size, ts, pixel_per_pt); + + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); + + if let Some(fill) = page.fill_or_white() { + if let Paint::Solid(color) = fill { + canvas.fill(paint::to_sk_color(color)); + } else { + let rect = Geometry::Rect(page.frame.size()).filled(fill); + shape::render_shape(&mut canvas, state, &rect); + } + } + + render_frame(&mut canvas, state, &page.frame); canvas } /// Export a document with potentially multiple pages into a single raster image. -/// -/// The gap will be added between the individual frames. pub fn render_merged( document: &Document, pixel_per_pt: f32, - frame_fill: Color, gap: Abs, - gap_fill: Color, + fill: Option, ) -> sk::Pixmap { - let pixmaps: Vec<_> = document - .pages - .iter() - .map(|page| render(&page.frame, pixel_per_pt, frame_fill)) - .collect(); + let pixmaps: Vec<_> = + document.pages.iter().map(|page| render(page, pixel_per_pt)).collect(); let gap = (pixel_per_pt * gap.to_f32()).round() as u32; let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); @@ -53,7 +57,9 @@ pub fn render_merged( + gap * pixmaps.len().saturating_sub(1) as u32; let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(gap_fill)); + if let Some(fill) = fill { + canvas.fill(paint::to_sk_color(fill)); + } let mut y = 0; for pixmap in pixmaps { diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 01ed3faee..145e23f86 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -11,11 +11,11 @@ use std::fmt::{self, Display, Formatter, Write}; use ecow::EcoString; use ttf_parser::OutlineBuilder; use typst::layout::{ - Abs, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, + Abs, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Ratio, Size, Transform, }; use typst::model::Document; use typst::utils::hash128; -use typst::visualize::{Gradient, Pattern}; +use typst::visualize::{Geometry, Gradient, Pattern}; use xmlwriter::XmlWriter; use crate::paint::{GradientRef, PatternRef, SVGSubGradient}; @@ -23,12 +23,12 @@ use crate::text::RenderedGlyph; /// Export a frame into a SVG file. #[typst_macros::time(name = "svg")] -pub fn svg(frame: &Frame) -> String { +pub fn svg(page: &Page) -> String { let mut renderer = SVGRenderer::new(); - renderer.write_header(frame.size()); + renderer.write_header(page.frame.size()); - let state = State::new(frame.size(), Transform::identity()); - renderer.render_frame(state, Transform::identity(), frame); + let state = State::new(page.frame.size(), Transform::identity()); + renderer.render_page(state, Transform::identity(), page); renderer.finalize() } @@ -57,7 +57,7 @@ pub fn svg_merged(document: &Document, padding: Abs) -> String { for page in &document.pages { let ts = Transform::translate(x, y); let state = State::new(page.frame.size(), Transform::identity()); - renderer.render_frame(state, ts, &page.frame); + renderer.render_page(state, ts, page); y += page.frame.height() + padding; } @@ -176,6 +176,16 @@ impl SVGRenderer { self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml"); } + /// Render a page with the given transform. + fn render_page(&mut self, state: State, ts: Transform, page: &Page) { + if let Some(fill) = page.fill_or_white() { + let shape = Geometry::Rect(page.frame.size()).filled(fill); + self.render_shape(state, &shape); + } + + self.render_frame(state, ts, &page.frame); + } + /// Render a frame with the given transform. fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { self.xml.start_element("g"); diff --git a/crates/typst/src/layout/page.rs b/crates/typst/src/layout/page.rs index cf8989175..ca2a0ce91 100644 --- a/crates/typst/src/layout/page.rs +++ b/crates/typst/src/layout/page.rs @@ -24,7 +24,7 @@ use crate::layout::{ use crate::model::Numbering; use crate::text::TextElem; use crate::utils::{NonZeroExt, Numeric, Scalar}; -use crate::visualize::Paint; +use crate::visualize::{Color, Paint}; /// Layouts its child onto one or multiple pages. /// @@ -178,12 +178,20 @@ pub struct PageElem { #[default(NonZeroUsize::ONE)] pub columns: NonZeroUsize, - /// The page's background color. + /// The page's background fill. /// - /// This instructs the printer to color the complete page with the given - /// color. If you are considering larger production runs, it may be more - /// environmentally friendly and cost-effective to source pre-dyed pages and - /// not set this property. + /// Setting this to something non-transparent instructs the printer to color + /// the complete page. If you are considering larger production runs, it may + /// be more environmentally friendly and cost-effective to source pre-dyed + /// pages and not set this property. + /// + /// When set to `{none}`, the background becomes transparent. Note that PDF + /// pages will still appear with a (usually white) background in viewers, + /// but they are conceptually transparent. (If you print them, no color is + /// used for the background.) + /// + /// The default of `{auto}` results in `{none}` for PDF output, and + /// `{white}` for PNG and SVG. /// /// ```example /// #set page(fill: rgb("444352")) @@ -191,7 +199,7 @@ pub struct PageElem { /// *Dark mode enabled.* /// ``` #[borrowed] - pub fill: Option, + pub fill: Smart>, /// How to [number]($numbering) the pages. /// @@ -555,13 +563,10 @@ impl PageLayout<'_> { } } - if let Some(fill) = fill { - frame.fill(fill.clone()); - } - page_counter.visit(engine, &frame)?; pages.push(Page { frame, + fill: fill.clone(), numbering: numbering.clone(), number: page_counter.logical(), }); @@ -578,6 +583,15 @@ impl PageLayout<'_> { pub struct Page { /// The frame that defines the page. pub frame: Frame, + /// How the page is filled. + /// + /// - When `None`, the background is transparent. + /// - When `Auto`, the background is transparent for PDF and white + /// for raster and SVG targets. + /// + /// Exporters should access the resolved value of this property through + /// `fill_or_transparent()` or `fill_or_white()`. + pub fill: Smart>, /// The page's numbering. pub numbering: Option, /// The logical page number (controlled by `counter(page)` and may thus not @@ -585,6 +599,22 @@ pub struct Page { pub number: usize, } +impl Page { + /// Get the configured background or `None` if it is `Auto`. + /// + /// This is used in PDF export. + pub fn fill_or_transparent(&self) -> Option { + self.fill.clone().unwrap_or(None) + } + + /// Get the configured background or white if it is `Auto`. + /// + /// This is used in raster and SVG export. + pub fn fill_or_white(&self) -> Option { + self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into())) + } +} + /// Specification of the page's margins. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Margin { diff --git a/docs/src/main.rs b/docs/src/main.rs index f4414b109..3f53dfc84 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -3,7 +3,6 @@ use std::path::{Path, PathBuf}; use clap::Parser; use typst::model::Document; -use typst::visualize::Color; use typst_docs::{provide, Html, Resolver}; use typst_render::render; @@ -35,8 +34,8 @@ impl<'a> Resolver for CliResolver<'a> { ); } - let frame = &document.pages.first().expect("page 0").frame; - let pixmap = render(frame, 2.0, Color::WHITE); + let page = document.pages.first().expect("page 0"); + let pixmap = render(page, 2.0); let filename = format!("{hash:x}.png"); let path = self.assets_dir.join(&filename); fs::create_dir_all(path.parent().expect("parent")).expect("create dir"); diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index c9536150f..fa9397814 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -6,7 +6,6 @@ use typst::foundations::{Bytes, Datetime}; use typst::syntax::{FileId, Source}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::visualize::Color; use typst::{Library, World}; struct FuzzWorld { @@ -68,7 +67,7 @@ fuzz_target!(|text: &str| { let world = FuzzWorld::new(text); if let Ok(document) = typst::compile(&world).output { if let Some(page) = document.pages.first() { - std::hint::black_box(typst_render::render(&page.frame, 1.0, Color::WHITE)); + std::hint::black_box(typst_render::render(page, 1.0)); } } comemo::evict(10); diff --git a/tests/ref/page-fill-none.png b/tests/ref/page-fill-none.png new file mode 100644 index 0000000000000000000000000000000000000000..d225135f428319aef5a3a40c31b9b227b1515279 GIT binary patch literal 97 zcmeAS@N?(olHy`uVBq!ia0vp^6+mpn$P6UcX77 tF{I*Fvc#7I|Lg_+NI&H_{wKi5aQ51q^L`Q zUVA_F-hX?)^<`awKdgFR{)BSsS9~wPZ^Qbu81akofsIP5RD4$?;83y@UseMSP$Bq#{(eFm)?-10GEi?t zeHT9PAh`5p-S8CXz%!L+0F@4f>->;!q$HVxCj0~XB(y>sf3-^h0000Pt12GIlEl>l26d?Atf$Km%(tk}yK;y9q1OCj1{k3n+%vur*cw4r4 z`40N>GPf{in?3Z|W)qV(yXdspLMnF=K+-6b`}laT{;2hoNPn&UUDtJAShvETXjpbH z-Aor~lO(eF+<$?K#Anym1TK>HT81o8NphAD)xqE@@Uwin3bcdNfjw#qt^#`i-J*=(D$oL$CGp`Z(4x>sSueN>v?xqU;408!1ON|Kt^zGa_gH?p z3bd$?K;n#0fq(rjF(k>LmbglMje3ndSWek4u-^kP)lsiNiv-nFne75CfS4+?T_8ru zM{Wy*H9yEh2t)xai=_&zHnDkZs0-vUn$uO_I&#JZmMgTA{3rQO@}J~C$$ygnB>zeN zll)(k{J*%-KkJq+kjdZHyZt5c{O$Srzdhr52mb*JX%d!?VbHKr00000NkvXXu0mjf DZQjAq diff --git a/tests/src/run.rs b/tests/src/run.rs index 3db03ba43..9681ae4cc 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -359,13 +359,8 @@ fn render(document: &Document, pixel_per_pt: f32) -> sk::Pixmap { } let gap = Abs::pt(1.0); - let mut pixmap = typst_render::render_merged( - document, - pixel_per_pt, - Color::WHITE, - gap, - Color::BLACK, - ); + let mut pixmap = + typst_render::render_merged(document, pixel_per_pt, gap, Some(Color::BLACK)); let gap = (pixel_per_pt * gap.to_pt() as f32).round(); diff --git a/tests/suite/layout/page.typ b/tests/suite/layout/page.typ index f833af59b..0e1f77295 100644 --- a/tests/suite/layout/page.typ +++ b/tests/suite/layout/page.typ @@ -66,7 +66,13 @@ // Test page fill. #set page(width: 80pt, height: 40pt, fill: eastern) #text(15pt, font: "Roboto", fill: white, smallcaps[Typst]) -#page(width: 40pt, fill: none, margin: (top: 10pt, rest: auto))[Hi] +#page(width: 40pt, fill: auto, margin: (top: 10pt, rest: auto))[Hi] + +--- page-fill-none --- +// Test disabling page fill. +// The PNG is filled with black anyway due to the test runner. +#set page(fill: none) +#rect(fill: green) --- page-margin-uniform --- // Set all margins at once. diff --git a/tests/suite/visualize/pattern.typ b/tests/suite/visualize/pattern.typ index 08051ed20..b0c92efaf 100644 --- a/tests/suite/visualize/pattern.typ +++ b/tests/suite/visualize/pattern.typ @@ -21,24 +21,30 @@ --- pattern-relative-self --- // Test with relative set to `"self"` #let pat(..args) = pattern(size: (30pt, 30pt), ..args)[ + #set line(stroke: green) #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt)) #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt)) ] #set page(fill: pat(), width: 100pt, height: 100pt) - -#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt) +#rect( + width: 100%, + height: 100%, + fill: pat(relative: "self"), + stroke: 1pt + green, +) --- pattern-relative-parent --- // Test with relative set to `"parent"` -#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[ +#let pat(fill, ..args) = pattern(size: (30pt, 30pt), ..args)[ + #rect(width: 100%, height: 100%, fill: fill, stroke: none) #place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt)) #place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt)) ] -#set page(fill: pat(), width: 100pt, height: 100pt) +#set page(fill: pat(white), width: 100pt, height: 100pt) -#rect(fill: pat(relative: "parent"), width: 100%, height: 100%, stroke: 1pt) +#rect(fill: pat(none, relative: "parent"), width: 100%, height: 100%, stroke: 1pt) --- pattern-small --- // Tests small patterns for pixel accuracy. From 96d456e2672c05f0b9c042fb481b1748c013a63d Mon Sep 17 00:00:00 2001 From: Leedehai <18319900+Leedehai@users.noreply.github.com> Date: Sat, 20 Jul 2024 08:51:51 -0400 Subject: [PATCH 09/15] Move matrix layout logic's early exit upward (#4583) --- crates/typst/src/math/matrix.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/typst/src/math/matrix.rs b/crates/typst/src/math/matrix.rs index 34e0611a6..95164e822 100644 --- a/crates/typst/src/math/matrix.rs +++ b/crates/typst/src/math/matrix.rs @@ -458,6 +458,12 @@ fn layout_mat_body( gap: Axes>, span: Span, ) -> SourceResult { + let ncols = rows.first().map_or(0, |row| row.len()); + let nrows = rows.len(); + if ncols == 0 || nrows == 0 { + return Ok(Frame::soft(Size::zero())); + } + let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to); let half_gap = gap * 0.5; @@ -483,12 +489,6 @@ fn layout_mat_body( _ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke), }; - let ncols = rows.first().map_or(0, |row| row.len()); - let nrows = rows.len(); - if ncols == 0 || nrows == 0 { - return Ok(Frame::soft(Size::zero())); - } - // Before the full matrix body can be laid out, the // individual cells must first be independently laid out // so we can ensure alignment across rows and columns. From 46ef8e1dfa39d2ee953e7af722b3b6ff20907443 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 20 Jul 2024 14:52:17 +0200 Subject: [PATCH 10/15] Deprecate some things (#4562) --- crates/typst/src/foundations/styles.rs | 9 ++++++++- crates/typst/src/introspection/counter.rs | 17 ++++++++++++++--- crates/typst/src/introspection/locate.rs | 18 ++++++++++++------ crates/typst/src/introspection/query.rs | 14 +++++++++++--- crates/typst/src/introspection/state.rs | 19 ++++++++++++++++--- crates/typst/src/layout/measure.rs | 14 ++++++++++---- tests/suite/foundations/context.typ | 13 +++++++++++++ tests/suite/introspection/counter.typ | 2 +- tests/suite/layout/grid/footers.typ | 6 +++--- tests/suite/layout/grid/headers.typ | 4 ++-- tests/suite/layout/grid/rowspan.typ | 2 +- tests/suite/layout/spacing.typ | 2 +- tests/suite/styling/fold.typ | 5 +++++ 13 files changed, 97 insertions(+), 28 deletions(-) diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 42be89229..48009c8c3 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -7,7 +7,7 @@ use comemo::{Track, Tracked}; use ecow::{eco_vec, EcoString, EcoVec}; use smallvec::SmallVec; -use crate::diag::{SourceResult, Trace, Tracepoint}; +use crate::diag::{warning, SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr, @@ -33,6 +33,8 @@ use crate::utils::LazyHash; /// ``` #[func] pub fn style( + /// The engine. + engine: &mut Engine, /// The call site span. span: Span, /// A function to call with the styles. Its return value is displayed @@ -43,6 +45,11 @@ pub fn style( /// content that depends on the style context it appears in. func: Func, ) -> Content { + engine.sink.warn(warning!( + span, "`style` is deprecated"; + hint: "use a `context` expression instead" + )); + StyleElem::new(func).pack().spanned(span) } diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index 13ea4d142..2aefb68ec 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -5,7 +5,7 @@ use comemo::{Track, Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use smallvec::{smallvec, SmallVec}; -use crate::diag::{bail, At, HintedStrResult, SourceResult}; +use crate::diag::{bail, warning, At, HintedStrResult, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, @@ -464,6 +464,11 @@ impl Counter { if let Ok(loc) = context.location() { self.display_impl(engine, loc, numbering, both, context.styles().ok()) } else { + engine.sink.warn(warning!( + span, "`counter.display` without context is deprecated"; + hint: "use it in a `context` expression instead" + )); + Ok(CounterDisplayElem::new(self, numbering, both) .pack() .spanned(span) @@ -508,13 +513,19 @@ impl Counter { context: Tracked, /// The callsite span. span: Span, - /// _Compatibility:_ This argument only exists for compatibility with - /// Typst 0.10 and lower and shouldn't be used anymore. + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used + /// anymore. #[default] location: Option, ) -> SourceResult { if location.is_none() { context.location().at(span)?; + } else { + engine.sink.warn(warning!( + span, "calling `counter.final` with a location is deprecated"; + hint: "try removing the location argument" + )); } let sequence = self.sequence(engine)?; diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst/src/introspection/locate.rs index 373b1fe27..8991ae9be 100644 --- a/crates/typst/src/introspection/locate.rs +++ b/crates/typst/src/introspection/locate.rs @@ -1,6 +1,6 @@ use comemo::{Track, Tracked}; -use crate::diag::{HintedStrResult, SourceResult}; +use crate::diag::{warning, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed, @@ -29,11 +29,12 @@ use crate::syntax::Span; /// /// # Compatibility /// In Typst 0.10 and lower, the `locate` function took a closure that made the -/// current location in the document available (like [`here`] does now). -/// Compatibility with the old way will remain for a while to give package -/// authors time to upgrade. To that effect, `locate` detects whether it -/// received a selector or a user-defined function and adjusts its semantics -/// accordingly. This behaviour will be removed in the future. +/// current location in the document available (like [`here`] does now). This +/// usage pattern is deprecated. Compatibility with the old way will remain for +/// a while to give package authors time to upgrade. To that effect, `locate` +/// detects whether it received a selector or a user-defined function and +/// adjusts its semantics accordingly. This behaviour will be removed in the +/// future. #[func(contextual)] pub fn locate( /// The engine. @@ -56,6 +57,11 @@ pub fn locate( LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?) } LocateInput::Func(func) => { + engine.sink.warn(warning!( + span, "`locate` with callback function is deprecated"; + hint: "use a `context` expression instead" + )); + LocateOutput::Content(LocateElem::new(func).pack().spanned(span)) } }) diff --git a/crates/typst/src/introspection/query.rs b/crates/typst/src/introspection/query.rs index e416bfc9d..07f761a8d 100644 --- a/crates/typst/src/introspection/query.rs +++ b/crates/typst/src/introspection/query.rs @@ -1,9 +1,10 @@ use comemo::Tracked; -use crate::diag::HintedStrResult; +use crate::diag::{warning, HintedStrResult}; use crate::engine::Engine; use crate::foundations::{func, Array, Context, LocatableSelector, Value}; use crate::introspection::Location; +use crate::syntax::Span; /// Finds elements in the document. /// @@ -141,6 +142,8 @@ pub fn query( engine: &mut Engine, /// The callsite context. context: Tracked, + /// The span of the `query` call. + span: Span, /// Can be /// - an element function like a `heading` or `figure`, /// - a `{