From ed50661378f356e02c6ec943bc4840091d33cfbd Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 22 Nov 2021 14:30:43 +0100 Subject: [PATCH] Castable optional and smart values --- src/eval/value.rs | 144 +++++++++++++++++++++-------- src/library/deco.rs | 2 +- src/library/mod.rs | 8 +- src/library/page.rs | 24 ++--- src/library/shape.rs | 6 +- src/library/text.rs | 27 +++--- src/style/mod.rs | 35 +++---- tests/ref/elements/fill-stroke.png | Bin 0 -> 1942 bytes tests/ref/layout/page.png | Bin 5829 -> 4157 bytes tests/ref/layout/pagebreak.png | Bin 1254 -> 3145 bytes tests/typ/elements/fill-stroke.typ | 26 ++++++ tests/typ/layout/page.typ | 20 +--- tests/typ/layout/pagebreak.typ | 14 +++ tests/typ/text/features.typ | 8 ++ tests/typeset.rs | 4 +- 15 files changed, 215 insertions(+), 103 deletions(-) create mode 100644 tests/ref/elements/fill-stroke.png create mode 100644 tests/typ/elements/fill-stroke.typ diff --git a/src/eval/value.rs b/src/eval/value.rs index e224438ac..dec5c6c0e 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -252,43 +252,6 @@ pub trait Cast: Sized { fn cast(value: V) -> StrResult; } -impl Cast for Value { - fn is(_: &Value) -> bool { - true - } - - fn cast(value: Value) -> StrResult { - Ok(value) - } -} - -impl Cast> for T -where - T: Cast, -{ - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - T::cast(value.v) - } -} - -impl Cast> for Spanned -where - T: Cast, -{ - fn is(value: &Spanned) -> bool { - T::is(&value.v) - } - - fn cast(value: Spanned) -> StrResult { - let span = value.span; - T::cast(value.v).map(|t| Spanned::new(t, span)) - } -} - /// Implement traits for primitives. macro_rules! primitive { ( @@ -400,6 +363,113 @@ primitive! { Dict: "dictionary", Dict } primitive! { Template: "template", Template } primitive! { Function: "function", Func } +impl Cast for Value { + fn is(_: &Value) -> bool { + true + } + + fn cast(value: Value) -> StrResult { + Ok(value) + } +} + +impl Cast> for T +where + T: Cast, +{ + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + T::cast(value.v) + } +} + +impl Cast> for Spanned +where + T: Cast, +{ + fn is(value: &Spanned) -> bool { + T::is(&value.v) + } + + fn cast(value: Spanned) -> StrResult { + let span = value.span; + T::cast(value.v).map(|t| Spanned::new(t, span)) + } +} + +/// A value that can be automatically determined. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Smart { + /// The value should be determined smartly based on the + /// circumstances. + Auto, + /// A forced, specific value. + Custom(T), +} + +impl Smart { + /// Returns the contained custom value or a provided default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Self::Auto => default, + Self::Custom(x) => x, + } + } +} + +impl Default for Smart { + fn default() -> Self { + Self::Auto + } +} + +impl Cast for Option +where + T: Cast, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::None => Ok(None), + v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")), + } + } +} + +impl Cast for Smart +where + T: Cast, +{ + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) || T::is(value) + } + + fn cast(value: Value) -> StrResult { + match value { + Value::Auto => Ok(Self::Auto), + v => T::cast(v) + .map(Self::Custom) + .map_err(|msg| with_alternative(msg, "auto")), + } + } +} + +/// Transform `expected X, found Y` into `expected X or A, found Y`. +fn with_alternative(msg: String, alt: &str) -> String { + let mut parts = msg.split(", found "); + if let (Some(a), Some(b)) = (parts.next(), parts.next()) { + format!("{} or {}, found {}", a, alt, b) + } else { + msg + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/library/deco.rs b/src/library/deco.rs index 1f8c051f2..cb0656892 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -17,7 +17,7 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult { } fn line_impl(args: &mut Args, kind: LineKind) -> TypResult { - let stroke = args.named("stroke")?.or_else(|| args.find()).map(Paint::Solid); + let stroke = args.named("stroke")?.or_else(|| args.find()); let thickness = args.named::("thickness")?.or_else(|| args.find()); let offset = args.named("offset")?; let extent = args.named("extent")?.unwrap_or_default(); diff --git a/src/library/mod.rs b/src/library/mod.rs index 7b8acf9ec..6260e6fcc 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -26,7 +26,7 @@ mod prelude { pub use std::rc::Rc; pub use crate::diag::{At, TypResult}; - pub use crate::eval::{Args, EvalContext, Template, Value}; + pub use crate::eval::{Args, EvalContext, Smart, Template, Value}; pub use crate::frame::*; pub use crate::geom::*; pub use crate::layout::*; @@ -144,3 +144,9 @@ dynamic! { FontFamily: "font family", Value::Str(string) => Self::Named(string.to_lowercase()), } + +castable! { + Paint, + Expected: "color", + Value::Color(color) => Paint::Solid(color), +} diff --git a/src/library/page.rs b/src/library/page.rs index 20871bd9b..b256a5211 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -12,13 +12,13 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult { let paper = args.named::("paper")?.or_else(|| args.find()); let width = args.named("width")?; let height = args.named("height")?; + let flip = args.named("flip")?; let margins = args.named("margins")?; let left = args.named("left")?; let top = args.named("top")?; let right = args.named("right")?; let bottom = args.named("bottom")?; - let flip = args.named("flip")?; - let fill = args.named("fill")?.map(Paint::Solid); + let fill = args.named("fill")?; ctx.template.modify(move |style| { let page = style.page_mut(); @@ -33,37 +33,37 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult { page.size.w = width; } + if flip.unwrap_or(false) { + std::mem::swap(&mut page.size.w, &mut page.size.h); + } + if let Some(height) = height { page.class = PaperClass::Custom; page.size.h = height; } if let Some(margins) = margins { - page.margins = Sides::splat(Some(margins)); + page.margins = Sides::splat(margins); } if let Some(left) = left { - page.margins.left = Some(left); + page.margins.left = left; } if let Some(top) = top { - page.margins.top = Some(top); + page.margins.top = top; } if let Some(right) = right { - page.margins.right = Some(right); + page.margins.right = right; } if let Some(bottom) = bottom { - page.margins.bottom = Some(bottom); - } - - if flip.unwrap_or(false) { - std::mem::swap(&mut page.size.w, &mut page.size.h); + page.margins.bottom = bottom; } if let Some(fill) = fill { - page.fill = Some(fill); + page.fill = fill; } }); diff --git a/src/library/shape.rs b/src/library/shape.rs index abf927e46..f47da82f7 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -58,11 +58,11 @@ fn shape_impl( }; // Parse fill & stroke. - let fill = args.named("fill")?.map(Paint::Solid); + let fill = args.named("fill")?.unwrap_or(None); let stroke = match (args.named("stroke")?, args.named("thickness")?) { (None, None) => fill.is_none().then(|| default), - (color, thickness) => Some(Stroke { - paint: color.map(Paint::Solid).unwrap_or(default.paint), + (color, thickness) => color.unwrap_or(Some(default.paint)).map(|paint| Stroke { + paint, thickness: thickness.unwrap_or(default.thickness), }), }; diff --git a/src/library/text.rs b/src/library/text.rs index d0b5c8e6c..c0ee80e15 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -93,18 +93,16 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult { castable! { StylisticSet, - Expected: "none or integer", - Value::None => Self(None), + Expected: "integer", Value::Int(v) => match v { - 1 ..= 20 => Self(Some(v as u8)), + 1 ..= 20 => Self::new(v as u8), _ => Err("must be between 1 and 20")?, }, } castable! { NumberType, - Expected: "auto or string", - Value::Auto => Self::Auto, + Expected: "string", Value::Str(string) => match string.as_str() { "lining" => Self::Lining, "old-style" => Self::OldStyle, @@ -114,8 +112,7 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult { castable! { NumberWidth, - Expected: "auto or string", - Value::Auto => Self::Auto, + Expected: "string", Value::Str(string) => match string.as_str() { "proportional" => Self::Proportional, "tabular" => Self::Tabular, @@ -629,8 +626,8 @@ fn tags(features: &FontFeatures) -> Vec { } let storage; - if let StylisticSet(Some(set @ 1 ..= 20)) = features.stylistic_set { - storage = [b's', b's', b'0' + set / 10, b'0' + set % 10]; + if let Some(set) = features.stylistic_set { + storage = [b's', b's', b'0' + set.get() / 10, b'0' + set.get() % 10]; feat(&storage, 1); } @@ -648,15 +645,15 @@ fn tags(features: &FontFeatures) -> Vec { } match features.numbers.type_ { - NumberType::Auto => {} - NumberType::Lining => feat(b"lnum", 1), - NumberType::OldStyle => feat(b"onum", 1), + Smart::Auto => {} + Smart::Custom(NumberType::Lining) => feat(b"lnum", 1), + Smart::Custom(NumberType::OldStyle) => feat(b"onum", 1), } match features.numbers.width { - NumberWidth::Auto => {} - NumberWidth::Proportional => feat(b"pnum", 1), - NumberWidth::Tabular => feat(b"tnum", 1), + Smart::Auto => {} + Smart::Custom(NumberWidth::Proportional) => feat(b"pnum", 1), + Smart::Custom(NumberWidth::Tabular) => feat(b"tnum", 1), } match features.numbers.position { diff --git a/src/style/mod.rs b/src/style/mod.rs index 4a8830f82..45dbeb546 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use ttf_parser::Tag; +use crate::eval::Smart; use crate::font::*; use crate::geom::*; use crate::util::EcoString; @@ -70,7 +71,7 @@ pub struct PageStyle { pub size: Size, /// The amount of white space on each side of the page. If a side is set to /// `None`, the default for the paper class is used. - pub margins: Sides>, + pub margins: Sides>, /// The background fill of the page. pub fill: Option, } @@ -94,7 +95,7 @@ impl Default for PageStyle { Self { class: paper.class(), size: paper.size(), - margins: Sides::splat(None), + margins: Sides::splat(Smart::Auto), fill: None, } } @@ -301,7 +302,7 @@ pub struct FontFeatures { /// Whether to apply stylistic alternates. ("salt") pub alternates: bool, /// Which stylistic set to apply. ("ss01" - "ss20") - pub stylistic_set: StylisticSet, + pub stylistic_set: Option, /// Configuration of ligature features. pub ligatures: LigatureFeatures, /// Configuration of numbers features. @@ -316,7 +317,7 @@ impl Default for FontFeatures { kerning: true, smallcaps: false, alternates: false, - stylistic_set: StylisticSet::default(), + stylistic_set: None, ligatures: LigatureFeatures::default(), numbers: NumberFeatures::default(), raw: vec![], @@ -326,11 +327,17 @@ impl Default for FontFeatures { /// A stylistic set in a font face. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct StylisticSet(pub Option); +pub struct StylisticSet(u8); -impl Default for StylisticSet { - fn default() -> Self { - Self(None) +impl StylisticSet { + /// Creates a new set, clamping to 1-20. + pub fn new(index: u8) -> Self { + Self(index.clamp(1, 20)) + } + + /// Get the value, guaranteed to be 1-20. + pub fn get(self) -> u8 { + self.0 } } @@ -359,9 +366,9 @@ impl Default for LigatureFeatures { #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct NumberFeatures { /// Whether to use lining or old-style numbers. - pub type_: NumberType, + pub type_: Smart, /// Whether to use proportional or tabular numbers. - pub width: NumberWidth, + pub width: Smart, /// How to position numbers vertically. pub position: NumberPosition, /// Whether to have a slash through the zero glyph. ("zero") @@ -373,8 +380,8 @@ pub struct NumberFeatures { impl Default for NumberFeatures { fn default() -> Self { Self { - type_: NumberType::Auto, - width: NumberWidth::Auto, + type_: Smart::Auto, + width: Smart::Auto, position: NumberPosition::Normal, slashed_zero: false, fractions: false, @@ -385,8 +392,6 @@ impl Default for NumberFeatures { /// Which kind of numbers / figures to select. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum NumberType { - /// Select the font's preference. - Auto, /// Numbers that fit well with capital text. ("lnum") Lining, /// Numbers that fit well into flow of upper- and lowercase text. ("onum") @@ -396,8 +401,6 @@ pub enum NumberType { /// The width of numbers / figures. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum NumberWidth { - /// Select the font's preference. - Auto, /// Number widths are glyph specific. ("pnum") Proportional, /// All numbers are of equal width / monospaced. ("tnum") diff --git a/tests/ref/elements/fill-stroke.png b/tests/ref/elements/fill-stroke.png new file mode 100644 index 0000000000000000000000000000000000000000..2d04b3dd6bf1872a5bcf62a43b412b95b2df5e46 GIT binary patch literal 1942 zcmZ`)X;hQf7X1jH`VovI3LFlZt4u zC5V9p8bBdH!2mBP_!i2f5+9;Ks|+II5Jbf%jeYAq`{%8-*SUACd+)mU?z8tf`3!$g z1RMgnleuIjQpLkEDh^WJm{i#xXP=s*t!qFC_wT}bqAG-FeCslDgYz(hf}Qi^tOd_12J+Pr~nX&e!q{xv^&dkwmK?@zOET{ z&+*8Lo_5FyCL{eCUi}W$&KsTa2tnUpA-1__-?B9VV}!0F!zS9#=h~g9o)f($Y$4b# zO=L%d(aT{lIH%Pk!3~dSW+D!chs9V>oe%(q<#U|M5umuK1*n#5hyVLWu0B89ZbtL2 z2lDT5{ZK$*gd)S<2?Vw=rP$)4L76)sBH-GmE0KU#sLvD?FR7oHPxt-a3KVgeG=@9` zzHS%($rUCoVYPCD08<}zB_AVQFNjFHXwG}ra52Cu9Sam6EqJ*Z+ zmXo8X{v>Y@V5A#?l>LKVMp_bUh$lX9{>VnB}((X5Zwe5NZ!3)nw>n~f#sQcXVK+**n6|AiXTgN}qo@i=ORjZ>*ln!A;o>vi`- z99fDXm^jzj^eBHIS=#IXlHaO|j_u%2GNmceAaL3I^5aAHrEdiQ`My0^;UoT|S%=!O zPs88aRAcJ{(C$*7K8{KHrtzkL+LvlH{)UxsvhIr=s8*mEyKJa~&A*4DTL#e@>$col z5+tN$pIk0M^W>vr+mKZM!(CQ>9n_zhnMM8z*m2(0{j^|H!SQi4|hstE1cBTijSXvJ8gvRKZM2jYEw=b z1V&M@lg?}_;3%9e^qx<{3e2dMzOFCmuUjuQ!sot3jlPsU~ z>ryERN*>dfY-b}^Eqy*WC z{2@r{D6F^LCBBMK9^vvA#(Q7ja+9vhA*NLA3@vAibNM8=^3{2vzj$P~r?B^e%|^B( z#{Q9K%Y3M-zl4TgPkX2Qcg&a!^=zdk@8-q40oWgWQc(HiS_JOK)0d5&%VmtK4!Ppj z`ezAJtT{>XRSS{VpoE0of1%My8Q(eE(;uAv>t=IXgyCnqMcRP453MPS>ZPTn#a4*1 z=gtv@Le^2B&c~pW-YE1Qnqu2nW3uvk)-itMp}ENoA2ZVMV408hUv;%NR!PN=KPJ@2 zugmCCpp@x^UB3z+7OA>Ed+{y&MfYNBD~0PNPTuuVQdj_pHOC{`xuy)6Eusf!sQ)3we>n0hWRPudTTH(>siLBi*y21 zq_gA!)podK=dwtSFigQ#PZFBFrFGGOQ-Sv=^NAHz#j}N5J$6@*RHA@|4Bw)M%Q|ug zejs7N*?Uj*NTPkN*^j=G9g5NuPqC$%bd3{VUx9z_Js+3;N7a7_`&;Zp9+ZFaqo!3U bPrWx&Bh5069veN;bU(n`-Jf1hjo|$WU7S#s literal 0 HcmV?d00001 diff --git a/tests/ref/layout/page.png b/tests/ref/layout/page.png index dae3d48827e0fd227ac8e8bc6b2e0ef8b1467e58..57e4b8f1ad3919cd0106979cfe2cb15cc0d9762c 100644 GIT binary patch literal 4157 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 TS&EXpwWVOzj#gDr&)EL~&b#|O literal 5829 zcmcIoXH-+!+CCvt6e$)2LJ0`!fI#2^BE1QS)De*;5L5)D7b&3yMNmONR6u%Q2%?lI zHAsn|L`6sfA{hD&kVHC!4&i2I^c(No`O2ELzWw9uefC*H1^hL0`0&DXNM!UPaOBl^d)^ApP}lqJUJ+4;Vi z`O(qQcT-aX z@w(HqsiUy4u%Mv8vG%n^Ra078T54*lZdtuj@k=BU85x-Yg(%NTJLVAoQ~K8KZrv8>jblcJW<;5L9K9nkUi$F#foPoI?bDpO zPEvbOw~sh_qHebw&Xyi&*ehQ{G~oW@vQz6hs9^bdy6$X^$64+;ovKNWJsByhK<*Qf z&0pGUD{?r=7mffvy;?r6X#8BGDlxoD&ef^fb^x1qs1`TlC@Dh|*m@1biQqzNN>Hi3 zgLS@aNf^R7*TfvkS}#5&&^xHpah4Z-fe``5-0#IZbWI)ya5b)7<-|aH$APaAHNpB| z#mMxDk**nW5<)XG@{PaGkEizaIy z47E9mc;+&V9uW{6m^^n25HdSs#lCUTd|jKz{8Z+>?(!X-@zllIksjimJwWBVX6|0};P}Xk^Z>mYeB=MIct( z<#*V3;=4;sh2xx=YpBC2n6aY_Ks1_M8i(wpaqZdsan_Qp8$x{LPX_>nXY;5wROC;{NdQI2y@cux#IyWVheZ!??| zz=%E|;fhXZKJ32ZCXF}aYp=)Vhi)9^!Zn-NPj?%A1MPQ;{toJQApe-7-pn^U^!qfe zv*^aG`r0DvhboWv!>9HFt>MX)cZ^SO`H z1g5blH@@ZwVR1iR6F%`C5@kTeqS{?8sTl94l{G^VI;T5fabUcm^JJi67AJ&~W~UVv ztPjOE-8`$AxiCO|yzK0Jbd$AaS%v7%*r-%kNizxMg;^=QdP#EKdhwm0Jv6M!cL9)d z>=*x2l+53)BltSWW@|;J9jlLqvcYAQPlPaIufrHd#7Zx5IlyVr+7^wNGIZxQ(S4Ch zu?ELU(@mH9Mh#^=0aZKX-CJGban^`%0MBR=R%1marngOqdXr;p86ANN8;P86&ap8M ze(0($sDj)f0gAJ7t09A|ZxY_Hh$+uVRMIWqSKi>ePaCUPaORS<@bW_^z1Z6#B| z0+i*Q*uGY1=KZHF|Ml&Tnh~Moo47IDNsX)g|4t+30aA-|Zn;0%fJDEa>I8M(h5cY8$qZId7#;C=(36ppf3DD6=IEtftr{h^2MZPy(7+@M3!@^N zy4xp0Ur^cfuEI^G?inZ-ZP{N>@^S8OJ`0V{a!L(&b|Lh|!5D2EM%?NTwHFZ|8v4<34cY+ZFHSo38 zfM9%-$x75^8L_+%*%maz!Md(n>W0u9v`US8PCEsgq?FDXd~-Eg=0s*cd0CuJYg(_S zT=DV*Y(}^{5elBbPJoBoJa2IxHv~EOPb?_zzgnw!v!YG2K!n7qWo>gUGA>2ErwxFCiwxoWbJiLf1g2YFrWM-m@!dIP zjd?GLl#=2#I(wW#vC^9!Qm@o}qBxKWU{2?(+Cv~fBdRPzUK?25diC{gv5Xi`wbU0_ zPaz|^vcUc)4KQ`m9)n;y+7?_5daZ}hKgaYlP>feceCW#b${TnFJrZtt?f71H@ zvrx3CK#*3^iBnDO7dQYPTWO61euIhS2ou>x0efi;+rqmw!G>T-;Vwg2NsnjpP+9M1 z8Mf|0oIv5rDU<^Q{zYrRyI2f~7(1&oY0XE66=>!I$Q)>Q6c+uVD0Ikj| z`*`1d`Nj~N9#LaXRvfj|UR|gqZbXCR?Ay^* zcxwls@=41yM!;G<>qDC@hk~<|LQaR}?!eT%sk!^C;c|diNaZ3gJUO83Sq+6P{4x}e zvaD|~F%D0Vr}y4|rkOsBxeQqIU76X~g9}z;OuW7%l;>Rx4t!Vdm~-GMmhGd?aPr&7 zVGCMwO=^3GgFplCfB`oTBo4ALBhTM*SjGSDaZC}r;>?oZ-V70mUo=K_qW9hu0%c}s z8#43v3@hOw9qW~zodTFKwotND^v8NRhj^hl+r~1+{tb#7emk9RNsT5_LQb}bEawTo zr#z>nktADnP1js#TKu4Z)Wgtrj)kF(Pc88rujr4bnpsCjW~&12eMg5A{g+O<7{Y?G zI3>ipV+Tq|dYLV4Hw^s&cXOzBzyVVAVPRH=8cIb| z7!aT-p4qpgNVX$!`DEJmDgm?vbYtXHWRY9g$I;t>V(!f$qjXZUxVWCsdkgZ_Q0c9= zkqMkg`p2k|Nf%`c^I|xkg8iLlJWdE^N63jL3zs_lks&~A`s;B9%zf#95T0} z<;iNSC52L+B|-j+(xclW!tnOA>b_Tg81?&3qH_EV z;E!{Tv@0y5=~RGs;Htinhe zS^4J=cC+X8Hel09no|`(*a@$phLb$n0_T(r_oO|n?!z%XfvnG3j#`pWPV!WY zgSIFg6tEj*r~Lvv*3tgbQQR0DE)IP*$s~Ki4X>$KY$)l+ESMT=&@HG%*%7d}VTYbKCMVw=_VUB6niAp6R?nSt2Y&JY^4jclm37+Fgt&WPI^Z#?XTmEo# z1`IYEf&bifMdF$Y@JV_w@d?Upnz$Ho&2u6ug5_n?5F zV*<0&;;}Hoc+g#C1P&Eai+MpIePq{u90(fI)D&bmymiE>b_J0q!z`-J&2(U2_EmbJ zPYlc&RyQr>TS^6WwophQMS1llnu~&Dfc`(U^_?D0jGyzwk4u_#Byi(aOaM&;E>#{w zVG9^0vC1`YFNrN9kNsl%4dQ}!#1H~FE)egCNw7#7X+w)mJAmV^eI*D~t=buY6+*7Q z>pcS(wx$Y8MeH4-x_JmA2V8?CEQ_BN$2#oF2o8NHsYhOrkd^0v-|08D;w!IeSYkQ9 z;Y^}lm~+jOjw?ulY6F_z_})UzgPcQ*kYAb(Hu_p1*4P#6fU$fYbH_{CuagM?rbt)6 zs>eh9()8)*sHbyvGr*^)%4d^Ako_X|S$;!724pVq<|zvCLf9*BG3_H_rgQrNIpg$5 zF!20=Ilfr~)_kV@#)Q4=Ni=AB`6)OmKL|8YaYDpZTt;l9j{cXGMq>!$(tq*qNQF1w z-|z^kTL#}KzAN&d{<@@Sw1NC~ttfhK#dY&ZS-A=Pz^}PiGO8^3%sEZxeHu}-1i8a` zX0Eu4>PL|u+Aeks2Rf-s@ay!q{>0XN?EUM!V@Ey0k48&B>6z=74i4J#E{?;o+|=uQ z6yJ;0hQO^ zue+SqS{g~p<%YbomFCA|3rm*-^<*Uup2ueNkbq5aP(=#+^pM?Y#Ou0e;t9M+;Y9b^ zp}e9&v;q$goJ8#rT#)+m>JTFVi|(u?d2~(63L+z?r4~zw{k}hP|9cjQJ^8osW&SMA zdGK4wS8qH`HvD%T*~}xMvk%TEQ2QHUj|aD^3Z2b@2C`+sW=u2N-IXn4VPsxP2P{+z zg3B*?obtk&k-!T?h2`g9uQEE*0tCinmE{{>ja%F5NzB|1irjsxou>|eC2Yfyz1_U847TG2c#Z&hDVfuk3*NKdge zUbJV(AY|f-u{5AIGu9vjX&zrhH4eVa;V4pPaBwkz@Q>JLGwxI)lk`_nuOC^SLPPiS z2l>sjguOg={+AK|5XPU%oZk~G3!UmHy^G%<8-@G_vuSHa)GvagWWeWCzy7bIbzZv6 zwu<*yGwDP%tc5PNT}yP$ivC%4)5jpiJG_8#}kli zK#CQCPL+cQBj7j?vY_pXJ;+b1WsM|7XaUR-pCVsFQdE({SsS#JUa56Gs50b^^Co6B zj})UO8&VP|Gt(03qryK%9NegA6~t0e3HCZB57t2q19a?y1TnZP|PUkD*v| zPd6^vcB0JMm3Dj^-5hKQS|-7R>)t4Kb`Y%wP3n%J}$>= z@zBCFB08sCY`vd?Xm-&ZF@Fw?b*<|6SCwWuhHvkSI%lR!X{WAi9$8;mWwohxMxY-u zXK>4^H5yak(QarCXh}={~AtriCcHd)e1qcy{u6Y z&D>lcpqK*7IeGGC6+CwAm+pdSA;J^1@P}LSVn$~ zo?h9M^7}kZz~Kk0+c#=SUa2ur+p7K_SP$lL{&eMW3T$`HUT9N%{TN^Uiu?k8ht;61 z093r6wUl`R!0FB>8C30pfP!FGzjc`cP3F!tC$Mh!GmY@>UNVQDpH5!`mnJpk#4?v! z-`QO(DeleS>pieAYNtIhT;O^krm=aay2Wd7)|dGcYa7e&$(pDn&%e^aALqKT-z(9U cGh49%&XYLSKVQJq$ns330sQ8_0B=q!XaE2J diff --git a/tests/ref/layout/pagebreak.png b/tests/ref/layout/pagebreak.png index 043b508645833d686982035cf03a1ffe6b25f424..fbb1d00c5feaa89b8f270c575883ab9ecc6f5563 100644 GIT binary patch literal 3145 zcmbuCcT`i^7RDJ3a7B=0J+LuAVxkD4s5B{pK>-6IV+2tkLJ*J^0wN{SoDjhYh#@p- zf*3#`^b$iAd@yvJ0HH`3lz~AAAiV^A(Ru4RYsq`_)|!9Ly7!!W?>c9H-~R1=fBNB) zv6#qV5di@KG4zG=7y*IrKmr20cLV$h0o;a~v4DUm7=0dP5irCZ;l^ZMlnNi$+1(+a zN)=5OJ@NU%<&@W{m!gkFheUy==Y9tlt_k1ib784gn|Idf{7T&R3KUS%%FrKHAanvS zGS~pv=={BD*De5hPvD#3lL`o3QpcN{F*V7l8Wk~U2@E4unBzUo(+3hpLqq?G?xu2sF(Ua!RC^?-Mjp0rmKs)4CS=b?ys3DL0|z8JWaO z-naCQ2evfGYJ-L*T(8iwM;vr+#R5Y|EuP+>XwoW_t3Z-ybCU$NstRZBwg5{Kkj19@ zbI^44n- zup#@{UhXyulS=rp>#a3J?b>!o^;{}~5M2K~IC6VswGzpOHwK+&sZ4ETr!Zy42sxqZ z$|2n^uO(kkL6|v(+zvHJeS$ZGZO`qU=vzvL zaAd$|`A+&~Sib}^_+kS+@pk8LR(E>iZ_SPOf&XnfUBwjCj-MJIgI;8oWoqx*~6nLGfkmoOm~wgypCo@o zWh|x>{RVFB-Y9@ycxFb^Zik0Hk+!YVy7Ar93cjfn*0zSF5s5$lbkaYn;K*^V$yBP3 z;mQLlK|pXx%J^VHd+OFJIEil2>+`O(Q0J{bgr5EQQyL@i?DO0HEs*W8XJ^0l7Y2*MWtrH)sh0VV$CX_1>GTbA z$(_Yn_xx-9#;YL`lff(6^_%NhU~WU;&2A46`4;ik^99S@*K78FKs z(cQpOcR@tZ2Rl#$alTgk5N>0x+;aZIQQ_t&DEg*rrA}7A5jFd1#bzG_;hS*d5Kg-r zPzjb3t*js2#3Ea5#t7fzZnN%^C&GD9!CP0?5%L;c`0vMaK;I_PpS?`3fbqG?hwGx{ zHI1-uvC5B2qd)ut^9{HDYTG)8(ESk!Lcbg3oBYNh*Z@v+95b73Wk~I{rqOdvizv|6 z5qzD@%#yAiomurH@;#kdb8@*##=QTx{o0)1A;2IpGUtLxJS%(HjOO6fC0Xq{dYU=W znOIof!>ehO&hwN4N{V~7*pfpIG&U}ge%Djx_Dr!UO;aVe1TDfr$j3<~Njuzh?z6@hK`6B471mg?n_FK%gGA;Bo%WWI{ z2wU4aM+YQ5yUdYIei*gqio0_JtLybd2ZeQLpL14s!Qv70D_!sloQz4QS=bw)M)N&8 z*0ry-&ix$pa}9(|bWk1}+;@+m z4>dgZe#r+BzZisc-p_Lj4;JKJO}ACOdZ8$pTFXaa@$lQiAWIP1+x0q}m~!JED9>~R z)`MzPJrS%DenN8VI2$@4em|^eo%!~9$eYy)UQ)yKKwG)Gha-x2B>cu5q-qk~c9 z49XpBx&qNZLa$$$S=1LqVGa*9Mjt}u4!_}0E^5I+OO&P$S##tDznr?F0<2JTxera*FUDeaYh1mVoU2|I zmPqeHA2@I{7f(|=-H#4SIRJUZ5pI@+>I!?nN?jm^QS!o$0Ofs*Hh@_@*sca#$87DWs_}Nyz`3-l&Q^tc=|v@K;9N2SAQs1 zoaQrKpchdA4`LL?D)^YOQs|{I2(GqkajCZ-O@cAP3gfaDLuw~%gT0zCJ`M%ITA%M9 z0PDRTr#3)$DWEshNX+t^Icqd-dS5j(>o#+B5N6dM^VaqVt{nA?GA$Y77KuPd1S=Om zV>^*`j|D^`ja&MB>E3!JjyJR|Fr}CBhS~0?8iU^Y17Y_o;K!}{KWvZUAR6D8woJW;&-3_5zvO0&KDZkll}$# CMH?6Z delta 1039 zcmV+q1n~RG80HC(7G41(2mk;80P)HoSO5S7tw}^dRCwC$-0f|%JQ#-Idjn?!*dS~G z8_*5N26O}1Al-m&KsSI57=aNy!u`q^OD#hEctqdIfe{mV33)uC-)W>x!E$T)^BqO&j6dCaoWkAk&^UG~;-a<9|! z`r5Xyu_5TPDb3AI5I4EzM!I#*7}grkB3F^lo&0dbe^5iEb~uu`vaD3KnHVG493?_p z4TGKJu4=39V`uMOLv6K~Y=?uEuKAsa>pspYGlS}oWKkfbg-aI@{3T)tC;tONc3s|* zau4AyR<*3m-C@l-IS8lH$AFOJE&;*6xNbdd5!5a{UNx6Jgvm8m`m$856IVF-12r)q zY=?vYf82>k)_z0?b(>uTHI4RSJ5kLl)oR^M@>mhvY44k>JnbUX`v_I8^;Ku-t?Ltw z_)E(E3M>+YrCIWEPLR?Mhh!I_6V2|b*Lra6zx!EbkbA5MI){<)0in9sWti8y2(xr< z9;nmix_qj9ubE>-&`yfC@%54~O>En>!fevlOY!Am>=wb-NYcb#Z)#uq^fuo9YF<2M zVoXG&a;=?`u&e5ni%7nl+koI~)*7`x`DVSmHb%t!mBB&3C(P4csU7Th4-i(*F(bqt zlYs&j4LA~k5JCtcgpkj}vKf<+2Q-tQ0xkl0G?Q@xA%FNAglV+rMo6U!o2uIQ+Sw2a zRPD1XDW~smSrOD; zlHA2)TZGU7bdcuc2;F*`!+6*tI44v1jeA@@;=2f{rSb2R)Ek5>42Q5-nl2|@EZK0| zph+5FGUBM?FeA%qa}MU&437-s?qM*G-flKZD6#QERHqY!-S%SitV zd?