work on cairo driver rust
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 19 Apr 2025 19:29:04 +0000 (12:29 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 19 Apr 2025 19:29:04 +0000 (12:29 -0700)
rust/Cargo.lock
rust/pspp/Cargo.toml
rust/pspp/src/output/cairo.rs [new file with mode: 0644]
rust/pspp/src/output/mod.rs
rust/pspp/src/output/page.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/output/render.rs
rust/pspp/src/output/text.rs

index 1a6e0fa4c472e395c566503429dd6b6161f6be39..625ffceed7bcc25e13017d78b1c3dcb1231fcf1f 100644 (file)
@@ -210,6 +210,29 @@ version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
 
+[[package]]
+name = "cairo-rs"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7"
+dependencies = [
+ "bitflags 2.6.0",
+ "cairo-sys-rs",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "cairo-sys-rs"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
 [[package]]
 name = "cc"
 version = "1.1.13"
@@ -219,6 +242,16 @@ dependencies = [
  "shlex",
 ]
 
+[[package]]
+name = "cfg-expr"
+version = "0.17.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -522,6 +555,17 @@ version = "0.3.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
 
+[[package]]
+name = "futures-executor"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
 [[package]]
 name = "futures-io"
 version = "0.3.30"
@@ -587,6 +631,91 @@ version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
 
+[[package]]
+name = "gio"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4f00c70f8029d84ea7572dd0e1aaa79e5329667b4c17f329d79ffb1e6277487"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "pin-project-lite",
+ "smallvec",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "160eb5250a26998c3e1b54e6a3d4ea15c6c7762a6062a19a7b63eff6e2b33f9e"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "glib"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "707b819af8059ee5395a2de9f2317d87a53dbad8846a2f089f0bb44703f37686"
+dependencies = [
+ "bitflags 2.6.0",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "smallvec",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68"
+dependencies = [
+ "heck",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.87",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8928869a44cfdd1fccb17d6746e4ff82c8f82e41ce705aa026a52ca8dc3aefb"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c773a3cb38a419ad9c26c81d177d96b4b08980e8bdbbf32dace883e96e96e7e3"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.14.5"
@@ -928,6 +1057,56 @@ version = "3.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
 
+[[package]]
+name = "pango"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1f5dc1b8cf9bc08bfc0843a04ee0fa2e78f1e1fa4b126844a383af4f25f0ec"
+dependencies = [
+ "gio",
+ "glib",
+ "libc",
+ "pango-sys",
+]
+
+[[package]]
+name = "pango-sys"
+version = "0.20.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dbb9b751673bd8fe49eb78620547973a1e719ed431372122b20abd12445bab5"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "pangocairo"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4690509a2fea2a6552a0ef8aa3e5f790c1365365ee0712afa1aedb39af3997b6"
+dependencies = [
+ "cairo-rs",
+ "glib",
+ "libc",
+ "pango",
+ "pangocairo-sys",
+]
+
+[[package]]
+name = "pangocairo-sys"
+version = "0.20.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be6ac24147911a6a46783922fc288cf02f67570bc0d360e563b5b26aead6767"
+dependencies = [
+ "cairo-sys-rs",
+ "glib-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
 [[package]]
 name = "parking_lot"
 version = "0.12.3"
@@ -989,6 +1168,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
 [[package]]
 name = "portable-atomic"
 version = "1.11.0"
@@ -1013,6 +1198,15 @@ dependencies = [
  "zerocopy",
 ]
 
+[[package]]
+name = "proc-macro-crate"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
+dependencies = [
+ "toml_edit",
+]
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.86"
@@ -1029,6 +1223,7 @@ dependencies = [
  "anyhow",
  "binrw",
  "bitflags 2.6.0",
+ "cairo-rs",
  "chardetng",
  "chrono",
  "clap",
@@ -1054,6 +1249,8 @@ dependencies = [
  "num-derive",
  "num-traits",
  "ordered-float",
+ "pango",
+ "pangocairo",
  "pspp-derive",
  "quick-xml",
  "rand",
@@ -1261,6 +1458,15 @@ dependencies = [
  "syn 2.0.87",
 ]
 
+[[package]]
+name = "serde_spanned"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+dependencies = [
+ "serde",
+]
+
 [[package]]
 name = "shlex"
 version = "1.3.0"
@@ -1338,6 +1544,25 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "system-deps"
+version = "7.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+
 [[package]]
 name = "termcolor"
 version = "0.3.6"
@@ -1434,6 +1659,40 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "toml"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
 [[package]]
 name = "tower"
 version = "0.4.13"
@@ -1597,6 +1856,12 @@ version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
+[[package]]
+name = "version-compare"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
+
 [[package]]
 name = "version_check"
 version = "0.9.5"
@@ -1858,6 +2123,15 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
+[[package]]
+name = "winnow"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "wit-bindgen-rt"
 version = "0.39.0"
index 07f6d4ebf36059d8194678f66e7a1d207d4ba1fa..3dc3a805cd398224a7572c7fb7888320a9b061ba 100644 (file)
@@ -42,6 +42,9 @@ color = { version = "0.2.3", features = ["serde"] }
 binrw = "0.14.1"
 ndarray = "0.16.1"
 derive_more = { version = "2.0.1", features = ["debug"] }
+cairo-rs = { version = "0.20.7", features = ["ps", "png", "pdf", "svg"] }
+pango = "0.20.9"
+pangocairo = "0.20.7"
 
 [target.'cfg(windows)'.dependencies]
 windows-sys = { version = "0.48.0", features = ["Win32_Globalization"] }
diff --git a/rust/pspp/src/output/cairo.rs b/rust/pspp/src/output/cairo.rs
new file mode 100644 (file)
index 0000000..b6cdeb9
--- /dev/null
@@ -0,0 +1,743 @@
+use std::{
+    borrow::Cow, cmp::min, f64::consts::PI, fmt::Write, ops::DerefMut, path::Path, sync::Arc,
+};
+
+use cairo::{Context, PdfSurface, Surface};
+use enum_map::{enum_map, EnumMap};
+use itertools::Itertools;
+use pango::{
+    parse_markup, AttrFloat, AttrFontDesc, AttrInt, AttrList, Attribute, FontDescription, FontMask,
+    Layout, Underline, Weight, SCALE, SCALE_SMALL,
+};
+use pangocairo::functions::show_layout;
+use smallvec::{smallvec, SmallVec};
+
+use crate::output::{
+    driver::Driver,
+    page::Setup,
+    pivot::{Color, Coord2, FontStyle, HorzAlign, Stroke},
+    render::{Device, DrawCell, Extreme, Params},
+    table::Content,
+    Item,
+};
+
+use super::pivot::{Axis2, BorderStyle, Rect2};
+
+/// Width of an ordinary line.
+const LINE_WIDTH: usize = LINE_SPACE / 2;
+
+/// Space between double lines.
+const LINE_SPACE: usize = SCALE as usize;
+
+/// Conversion from 1/96" units ("pixels") to Cairo/Pango units.
+fn px_to_xr(x: usize) -> usize {
+    x * 3 * (SCALE as usize * 72 / 96) / 3
+}
+
+fn xr_to_pt(x: usize) -> f64 {
+    x as f64 / SCALE as f64
+}
+
+fn xr_to_pango(x: usize) -> usize {
+    x
+}
+
+/// Conversion from 1/96" units ("pixels") to Cairo/Pango units.
+fn pxf_to_xr(x: f64) -> usize {
+    (x * (SCALE as f64 * 72.0 / 96.0)).round() as usize
+}
+
+pub struct Style {
+    /// Page size.
+    pub size: Coord2,
+
+    /// Minimum cell size to allow breaking.
+    pub min_break: EnumMap<Axis2, usize>,
+
+    /// The basic font.
+    pub font: FontDescription,
+
+    /// Foreground color.
+    pub fg: Color,
+
+    /// Use system colors?
+    pub use_system_colors: bool,
+
+    /// Vertical space between different output items.
+    pub object_spacing: usize,
+
+    /// Resolution, in units per inch, used for measuring font "points":
+    ///
+    /// - 72.0 if 1 pt is one device unit, e.g. for rendering to a surface
+    ///   created by [PsSurface::new] with its default transformation matrix of 72
+    ///   units/inch.p
+    ///
+    /// - 96.0 is traditional for a screen-based surface.
+    pub font_resolution: f64,
+}
+
+impl Style {
+    fn new_layout(&self, context: &Context) -> Layout {
+        let pangocairo_context = pangocairo::functions::create_context(context);
+        pangocairo::functions::context_set_resolution(&pangocairo_context, self.font_resolution);
+        Layout::new(&pangocairo_context)
+    }
+}
+
+pub struct CairoRenderer {
+    style: Arc<Style>,
+    params: Params,
+    context: Context,
+}
+
+impl CairoRenderer {
+    pub fn new(style: Arc<Style>, printing: bool, surface: &Surface) -> Self {
+        let context = Context::new(surface).unwrap();
+        let params = Params {
+            size: style.size,
+            font_size: {
+                let layout = style.new_layout(&context);
+                layout.set_font_description(Some(&style.font));
+                layout.set_text("0");
+                let char_size = layout.size();
+                enum_map! {
+                    Axis2::X => char_size.0.max(0) as usize,
+                    Axis2::Y => char_size.1.max(0) as usize
+                }
+            },
+            line_widths: enum_map! {
+                Stroke::None => 0,
+                Stroke::Solid | Stroke::Dashed => LINE_WIDTH,
+                Stroke::Thick => LINE_WIDTH * 2,
+                Stroke::Thin => LINE_WIDTH / 2,
+                Stroke::Double => LINE_WIDTH * 2 + LINE_SPACE,
+            },
+            px_size: Some(px_to_xr(1)),
+            min_break: style.min_break,
+            supports_margins: true,
+            rtl: false,
+            printing,
+            can_adjust_break: false, // XXX
+            can_scale: true,
+        };
+        Self {
+            style,
+            params,
+            context,
+        }
+    }
+}
+
+fn parse_font_style(font_style: &FontStyle) -> FontDescription {
+    let font = font_style.font.as_str();
+    let font = if font.eq_ignore_ascii_case("Monospaced") {
+        "Monospace"
+    } else {
+        font
+    };
+    let mut font_desc = FontDescription::from_string(font);
+    if !font_desc.set_fields().contains(FontMask::SIZE) {
+        let default_size = if font_style.size != 0 {
+            font_style.size * 1000
+        } else {
+            10_000
+        };
+        font_desc.set_size(((default_size as f64 / 1000.0) * (SCALE as f64)) as i32);
+    }
+    font_desc.set_weight(if font_style.bold {
+        Weight::Bold
+    } else {
+        Weight::Normal
+    });
+    font_desc.set_style(if font_style.italic {
+        pango::Style::Italic
+    } else {
+        pango::Style::Normal
+    });
+    font_desc
+}
+
+/// Deal with an oddity of the Unicode line-breaking algorithm (or perhaps in
+/// Pango's implementation of it): it will break after a period or a comma that
+/// precedes a digit, e.g. in `.000` it will break after the period.  This code
+/// looks for such a situation and inserts a U+2060 WORD JOINER to prevent the
+/// break.
+///
+/// This isn't necessary when the decimal point is between two digits
+/// (e.g. `0.000` won't be broken) or when the display width is not limited so
+/// that word wrapping won't happen.
+///
+/// It isn't necessary to look for more than one period or comma, as would
+/// happen with grouping like `1,234,567.89` or `1.234.567,89` because if groups
+/// are present then there will always be a digit on both sides of every period
+/// and comma.
+fn avoid_decimal_split(mut s: String) -> String {
+    if let Some(position) = s.find(&['.', ',']) {
+        let followed_by_digit = s[position + 1..]
+            .chars()
+            .next()
+            .is_some_and(|c| c.is_ascii_digit());
+        let not_preceded_by_digit = s[..position]
+            .chars()
+            .next_back()
+            .is_none_or(|c| !c.is_ascii_digit());
+        if followed_by_digit && not_preceded_by_digit {
+            s.insert(position + 1, '\u{2060}');
+        }
+    }
+    s
+}
+
+impl CairoRenderer {
+    fn layout_cell(&self, cell: &DrawCell, mut bb: Rect2, clip: &Rect2) -> Coord2 {
+        // XXX rotation
+        //let h = if cell.rotate { Axis2::Y } else { Axis2::X };
+
+        let layout = self.style.new_layout(&self.context);
+
+        let cell_font = if !cell.style.font_style.font.is_empty() {
+            Some(parse_font_style(&cell.style.font_style))
+        } else {
+            None
+        };
+        let font = cell_font.as_ref().unwrap_or(&self.style.font);
+        layout.set_font_description(Some(font));
+
+        let (body, suffixes) = cell.display().split_suffixes();
+        let horz_align = cell.horz_align(&body);
+        let body = body.to_string();
+
+        match horz_align {
+            HorzAlign::Decimal { offset, decimal } if !cell.rotate => {
+                let decimal_position = if let Some(position) = body.rfind(char::from(decimal)) {
+                    layout.set_text(&body[position..]);
+                    layout.set_width(-1);
+                    layout.size().0.max(0) as usize
+                } else {
+                    0
+                };
+                bb[Axis2::X].end -= pxf_to_xr(offset).saturating_sub(decimal_position);
+            }
+            _ => (),
+        }
+
+        let mut attrs = None;
+        let mut body = if cell.style.font_style.markup {
+            match parse_markup(&body, 0 as char) {
+                Ok((markup_attrs, string, _accel)) => {
+                    attrs = Some(markup_attrs);
+                    string.into()
+                }
+                Err(_) => body,
+            }
+        } else {
+            avoid_decimal_split(body)
+        };
+
+        if cell.style.font_style.underline {
+            attrs
+                .get_or_insert_default()
+                .insert(AttrInt::new_underline(Underline::Single));
+        }
+
+        if !suffixes.is_empty() {
+            let subscript_ofs = body.len();
+            #[allow(unstable_name_collisions)]
+            body.extend(suffixes.subscripts().intersperse(","));
+            let has_subscripts = subscript_ofs != body.len();
+
+            let footnote_ofs = body.len();
+            for (index, footnote) in suffixes.footnotes().enumerate() {
+                if index > 0 {
+                    body.push(',');
+                }
+                write!(&mut body, "{footnote}").unwrap();
+            }
+            let has_footnotes = footnote_ofs != body.len();
+
+            // Allow footnote markers to occupy the right margin.  That way,
+            // numbers in the column are still aligned.
+            if has_footnotes && horz_align == HorzAlign::Right {
+                // Measure the width of the footnote marker, so we know how much we
+                // need to make room for.
+                layout.set_text(&body[footnote_ofs..]);
+
+                let footnote_attrs = AttrList::new();
+                footnote_attrs.insert(AttrFloat::new_scale(SCALE_SMALL));
+                footnote_attrs.insert(AttrInt::new_rise(3000));
+                layout.set_attributes(Some(&footnote_attrs));
+                let footnote_width = layout.size().0.max(0) as usize;
+
+                // Bound the adjustment by the width of the right margin.
+                let right_margin =
+                    px_to_xr(cell.style.cell_style.margins[Axis2::X][1].max(0) as usize);
+                let footnote_adjustment = min(footnote_width, right_margin);
+
+                // Adjust the bounding box.
+                if cell.rotate {
+                    bb[Axis2::X].end = bb[Axis2::X].end.saturating_sub(footnote_adjustment);
+                } else {
+                    bb[Axis2::X].end += footnote_adjustment;
+                }
+
+                // Clean up.
+                layout.set_attributes(None);
+            }
+
+            fn with_start<T: DerefMut<Target = Attribute>>(index: usize, mut attr: T) -> T {
+                attr.deref_mut().set_start_index(index.try_into().unwrap());
+                attr
+            }
+            fn with_end<T: DerefMut<Target = Attribute>>(index: usize, mut attr: T) -> T {
+                attr.deref_mut().set_end_index(index.try_into().unwrap());
+                attr
+            }
+
+            // Set attributes.
+            let attrs = attrs.get_or_insert_default();
+            attrs.insert(with_start(subscript_ofs, AttrFontDesc::new(font)));
+            attrs.insert(with_start(subscript_ofs, AttrFloat::new_scale(SCALE_SMALL)));
+            if has_subscripts {
+                attrs.insert(with_start(
+                    subscript_ofs,
+                    with_end(footnote_ofs, AttrInt::new_rise(-3000)),
+                ));
+            }
+            if has_footnotes {
+                let rise = 3000; // XXX check look for superscript vs subscript
+                attrs.insert(with_start(footnote_ofs, AttrInt::new_rise(rise)));
+            }
+        }
+
+        layout.set_attributes(attrs.as_ref());
+        layout.set_text(&body);
+        layout.set_alignment(match horz_align {
+            HorzAlign::Right | HorzAlign::Decimal { .. } => pango::Alignment::Right,
+            HorzAlign::Left => pango::Alignment::Left,
+            HorzAlign::Center => pango::Alignment::Center,
+        });
+        if bb[Axis2::X].end == usize::MAX {
+            layout.set_width(-1);
+        } else {
+            layout.set_width(xr_to_pango(bb[Axis2::X].len()) as i32);
+        }
+
+        let size = layout.size();
+
+        if !clip.is_empty() {
+            self.context.save().unwrap();
+            if !cell.rotate {
+                xr_clip(&self.context, clip);
+            }
+            if cell.rotate {
+                let extra = bb[Axis2::X].len().saturating_sub(size.1.max(0) as usize);
+                let halign_offset = extra / 2;
+                self.context.translate(
+                    xr_to_pt(bb[Axis2::X].start + halign_offset),
+                    xr_to_pt(bb[Axis2::Y].end),
+                );
+                self.context.rotate(-PI / 2.0);
+            } else {
+                self.context
+                    .translate(xr_to_pt(bb[Axis2::X].start), xr_to_pt(bb[Axis2::Y].start));
+            }
+            show_layout(&self.context, &layout);
+            self.context.restore().unwrap();
+        }
+
+        layout.set_attributes(None);
+
+        Coord2::new(size.0.max(0) as usize, size.1.max(0) as usize)
+    }
+
+    fn do_draw_line(
+        &self,
+        x0: usize,
+        y0: usize,
+        x1: usize,
+        y1: usize,
+        stroke: Stroke,
+        color: Color,
+    ) {
+        self.context.new_path();
+        self.context.set_line_width(match stroke {
+            Stroke::Thick => LINE_WIDTH * 2,
+            Stroke::Thin => LINE_WIDTH / 2,
+            _ => LINE_WIDTH,
+        } as f64);
+        self.context.move_to(xr_to_pt(x0), xr_to_pt(y0));
+        self.context.line_to(xr_to_pt(x1), xr_to_pt(y1));
+        if !self.style.use_system_colors {
+            xr_set_color(&self.context, &color);
+        }
+        if stroke == Stroke::Dashed {
+            self.context.set_dash(&[2.0], 0.0);
+            let _ = self.context.stroke();
+            self.context.set_dash(&[], 0.0);
+        } else {
+            let _ = self.context.stroke();
+        }
+    }
+}
+
+fn xr_clip(context: &Context, clip: &Rect2) {
+    if clip[Axis2::X].end != usize::MAX || clip[Axis2::Y].end != usize::MAX {
+        let x0 = xr_to_pt(clip[Axis2::X].start);
+        let y0 = xr_to_pt(clip[Axis2::Y].start);
+        let x1 = xr_to_pt(clip[Axis2::X].end);
+        let y1 = xr_to_pt(clip[Axis2::Y].end);
+        context.rectangle(x0, y0, x1 - x0, y1 - y0);
+        context.clip();
+    }
+}
+
+fn xr_set_color(context: &Context, color: &Color) {
+    fn as_frac(x: u8) -> f64 {
+        x as f64 / 255.0
+    }
+
+    context.set_source_rgba(
+        as_frac(color.r),
+        as_frac(color.g),
+        as_frac(color.b),
+        as_frac(color.alpha),
+    );
+}
+
+fn xr_fill_rectangle(context: &Context, rectangle: Rect2) {
+    context.new_path();
+    context.set_line_width(xr_to_pt(LINE_WIDTH));
+
+    let x0 = xr_to_pt(rectangle[Axis2::X].start);
+    let y0 = xr_to_pt(rectangle[Axis2::Y].start);
+    let width = xr_to_pt(rectangle[Axis2::X].len());
+    let height = xr_to_pt(rectangle[Axis2::Y].len());
+    context.rectangle(x0, y0, width, height);
+    let _ = context.fill();
+}
+
+fn margin(cell: &DrawCell, axis: Axis2) -> usize {
+    px_to_xr(
+        cell.style.cell_style.margins[axis]
+            .iter()
+            .sum::<i32>()
+            .max(0) as usize,
+    )
+}
+
+impl Device for CairoRenderer {
+    fn params(&self) -> &Params {
+        &self.params
+    }
+
+    fn measure_cell_width(&self, cell: &DrawCell) -> EnumMap<Extreme, usize> {
+        fn add_margins(cell: &DrawCell, width: usize) -> usize {
+            if width > 0 {
+                width + margin(cell, Axis2::X)
+            } else {
+                0
+            }
+        }
+
+        /// An empty clipping rectangle.
+        fn clip() -> Rect2 {
+            Rect2::default()
+        }
+
+        enum_map![
+            Extreme::Min => {
+                let bb = Rect2::new(0..1, 0..usize::MAX);
+                add_margins(cell, self.layout_cell(cell, bb, &clip()).x())
+            }
+            Extreme::Max => {
+                let bb = Rect2::new(0..usize::MAX, 0..usize::MAX);
+                add_margins(cell, self.layout_cell(cell, bb, &clip()).x())
+            },
+        ]
+    }
+
+    fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize {
+        let margins = &cell.style.cell_style.margins;
+        let bb = Rect2::new(0..width - px_to_xr(margins[Axis2::X].len()), 0..usize::MAX);
+        self.layout_cell(cell, bb, &Rect2::default()).y() + margin(cell, Axis2::Y)
+    }
+
+    fn adjust_break(&self, cell: &Content, size: Coord2) -> usize {
+        todo!()
+    }
+
+    fn draw_line(&mut self, bb: Rect2, styles: EnumMap<Axis2, [BorderStyle; 2]>) {
+        let x0 = bb[Axis2::X].start;
+        let y0 = bb[Axis2::Y].start;
+        let x3 = bb[Axis2::X].end;
+        let y3 = bb[Axis2::Y].end;
+
+        let top = styles[Axis2::X][0].stroke;
+        let bottom = styles[Axis2::X][1].stroke;
+        let left = styles[Axis2::Y][0].stroke;
+        let right = styles[Axis2::Y][1].stroke;
+
+        let top_color = styles[Axis2::X][0].color;
+        let bottom_color = styles[Axis2::X][1].color;
+        let left_color = styles[Axis2::Y][0].color;
+        let right_color = styles[Axis2::Y][1].color;
+
+        // The algorithm here is somewhat subtle, to allow it to handle
+        // all the kinds of intersections that we need.
+        //
+        // Three additional ordinates are assigned along the X axis.  The first
+        // is `xc`, midway between `x0` and `x3`.  The others are `x1` and `x2`;
+        // for a single vertical line these are equal to `xc`, and for a double
+        // vertical line they are the ordinates of the left and right half of
+        // the double line.
+        //
+        // `yc`, `y1`, and `y2` are assigned similarly along the Y axis.
+        //
+        // The following diagram shows the coordinate system and output for
+        // double top and bottom lines, single left line, and no right line:
+        //
+        // ```
+        //             x0       x1 xc  x2      x3
+        //           y0 ________________________
+        //              |        #     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        // y1 = y2 = yc |#########     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        //              |        #     #       |
+        //           y3 |________#_____#_______|
+        // ```
+
+        // Offset from center of each line in a pair of double lines.
+        let double_line_ofs = (LINE_SPACE + LINE_WIDTH) / 2;
+
+        // Are the lines along each axis single or double?  (It doesn't make
+        // sense to have different kinds of line on the same axis, so we don't
+        // try to gracefully handle that case.)
+        let double_vert = top == Stroke::Double || bottom == Stroke::Double;
+        let double_horz = left == Stroke::Double || right == Stroke::Double;
+
+        // When horizontal lines are doubled, the left-side line along `y1`
+        // normally runs from `x0` to `x2`, and the right-side line along `y1`
+        // from `x3` to `x1`.  If the top-side line is also doubled, we shorten
+        // the `y1` lines, so that the left-side line runs only to `x1`, and the
+        // right-side line only to `x2`.  Otherwise, the horizontal line at `y =
+        // y1` below would cut off the intersection, which looks ugly:
+        //
+        // ```
+        //           x0       x1     x2      x3
+        //         y0 ________________________
+        //            |        #     #       |
+        //            |        #     #       |
+        //            |        #     #       |
+        //            |        #     #       |
+        //         y1 |#########     ########|
+        //            |                      |
+        //            |                      |
+        //         y2 |######################|
+        //            |                      |
+        //            |                      |
+        //         y3 |______________________|
+        // ```
+        //
+        // It is more of a judgment call when the horizontal line is single.  We
+        // choose to cut off the line anyhow, as shown in the first diagram
+        // above.
+        let shorten_y1_lines = top == Stroke::Double;
+        let shorten_y2_lines = bottom == Stroke::Double;
+        let shorten_yc_line = shorten_y1_lines && shorten_y2_lines;
+        let horz_line_ofs = if double_vert { double_line_ofs } else { 0 };
+        let xc = (x0 + x3) / 2;
+        let x1 = xc - horz_line_ofs;
+        let x2 = xc + horz_line_ofs;
+
+        let shorten_x1_lines = left == Stroke::Double;
+        let shorten_x2_lines = right == Stroke::Double;
+        let shorten_xc_line = shorten_x1_lines && shorten_x2_lines;
+        let vert_line_ofs = if double_horz { double_line_ofs } else { 0 };
+        let yc = (y0 + y3) / 2;
+        let y1 = yc - vert_line_ofs;
+        let y2 = yc + vert_line_ofs;
+
+        let horz_lines: SmallVec<[_; 2]> = if double_horz {
+            smallvec![(y1, shorten_y1_lines), (y2, shorten_y2_lines)]
+        } else {
+            smallvec![(yc, shorten_yc_line)]
+        };
+        for (y, shorten) in horz_lines {
+            if left != Stroke::None
+                && right != Stroke::None
+                && !shorten
+                && left_color == right_color
+            {
+                self.do_draw_line(x0, y, x3, y, left, left_color);
+            } else {
+                if left != Stroke::None {
+                    self.do_draw_line(x0, y, if shorten { x1 } else { x2 }, y, left, left_color);
+                }
+                if right != Stroke::None {
+                    self.do_draw_line(if shorten { x2 } else { x1 }, y, x3, y, right, right_color);
+                }
+            }
+        }
+
+        let vert_lines: SmallVec<[_; 2]> = if double_vert {
+            smallvec![(x1, shorten_x1_lines), (x2, shorten_x2_lines)]
+        } else {
+            smallvec![(xc, shorten_xc_line)]
+        };
+        for (x, shorten) in vert_lines {
+            if top != Stroke::None
+                && bottom != Stroke::None
+                && !shorten
+                && top_color == bottom_color
+            {
+                self.do_draw_line(x, y0, x, y3, top, top_color);
+            } else {
+                if top != Stroke::None {
+                    self.do_draw_line(x, y0, x, if shorten { y1 } else { y2 }, top, top_color);
+                }
+                if bottom != Stroke::None {
+                    self.do_draw_line(
+                        x,
+                        if shorten { y2 } else { y1 },
+                        x,
+                        y3,
+                        bottom,
+                        bottom_color,
+                    );
+                }
+            }
+        }
+    }
+
+    fn draw_cell(
+        &mut self,
+        draw_cell: &DrawCell,
+        alternate_row: bool,
+        mut bb: Rect2,
+        valign_offset: usize,
+        spill: EnumMap<Axis2, [usize; 2]>,
+        clip: &Rect2,
+    ) {
+        let fg = &draw_cell.style.font_style.fg[alternate_row as usize];
+        let bg = &draw_cell.style.font_style.bg[alternate_row as usize];
+
+        if (bg.r != 255 || bg.g != 255 || bg.b != 255) && bg.alpha != 0 {
+            self.context.save().unwrap();
+            let bg_clip = Rect2::from_fn(|axis| {
+                let start = if bb[axis].start == clip[axis].start {
+                    clip[axis].start.saturating_sub(spill[axis][0])
+                } else {
+                    clip[axis].start
+                };
+                let end = if bb[axis].end == clip[axis].end {
+                    clip[axis].end + spill[axis][1]
+                } else {
+                    clip[axis].end
+                };
+                start..end
+            });
+            xr_clip(&self.context, &bg_clip);
+            xr_set_color(&self.context, bg);
+            let x0 = bb[Axis2::X].start.saturating_sub(spill[Axis2::X][0]);
+            let y0 = bb[Axis2::Y].start.saturating_sub(spill[Axis2::X][1]);
+            let x1 = bb[Axis2::X].end + spill[Axis2::X][1];
+            let y1 = bb[Axis2::Y].end + spill[Axis2::Y][1];
+            xr_fill_rectangle(&self.context, Rect2::new(x0..x1, y0..y1));
+            self.context.restore().unwrap();
+        }
+
+        if !self.style.use_system_colors {
+            xr_set_color(&self.context, fg);
+        }
+
+        self.context.save().unwrap();
+        bb[Axis2::Y].start += valign_offset;
+        for axis in [Axis2::X, Axis2::Y] {
+            bb[axis].start += px_to_xr(draw_cell.style.cell_style.margins[axis][0].max(0) as usize);
+            bb[axis].end = bb[axis]
+                .end
+                .saturating_sub(draw_cell.style.cell_style.margins[axis][0].max(0) as usize);
+        }
+        if bb[Axis2::X].start < bb[Axis2::X].end && bb[Axis2::Y].start < bb[Axis2::Y].end {
+            self.layout_cell(draw_cell, bb, clip);
+        }
+        self.context.restore().unwrap();
+    }
+
+    fn scale(&mut self, factor: f64) {
+        self.context.scale(factor, factor);
+    }
+}
+
+pub struct CairoDriver {
+    renderer: CairoRenderer,
+}
+
+impl CairoDriver {
+    pub fn new(path: impl AsRef<Path>) -> CairoDriver {
+        fn scale(inches: f64) -> usize {
+            (inches * 72.0 * SCALE as f64).max(0.0).round() as usize
+        }
+
+        let page_setup = Setup::default();
+        let printable = page_setup.printable_size();
+        let size = Coord2::new(scale(printable[Axis2::X]), scale(printable[Axis2::Y]));
+        let font = FontStyle {
+            bold: false,
+            italic: false,
+            underline: false,
+            markup: false,
+            font: "Sans Serif".into(),
+            fg: [Color::BLACK, Color::BLACK],
+            bg: [Color::WHITE, Color::WHITE],
+            size: 10,
+        };
+        let font = parse_font_style(&font);
+        let style = Style {
+            size,
+            min_break: enum_map! {
+                Axis2::X => size[Axis2::X] / 2,
+                Axis2::Y => size[Axis2::Y] / 2,
+            },
+            font,
+            fg: Color::BLACK,
+            use_system_colors: false,
+            object_spacing: scale(page_setup.object_spacing),
+            font_resolution: 72.0,
+        };
+        let surface = PdfSurface::new(
+            page_setup.paper[Axis2::X] * 72.0,
+            page_setup.paper[Axis2::Y] * 72.0,
+            path,
+        )
+        .unwrap();
+        let renderer = CairoRenderer::new(Arc::new(style), false, &surface);
+        Self { renderer }
+    }
+}
+
+impl Driver for CairoDriver {
+    fn name(&self) -> Cow<'static, str> {
+        todo!()
+    }
+
+    fn write(&mut self, item: &Arc<Item>) {
+        todo!()
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use crate::output::cairo::CairoDriver;
+
+    #[test]
+    fn create() {
+        CairoDriver::new("test.pdf");
+    }
+}
index 2e356959c322132e388f3bb2b64fe537cef589c5..e2c194fccec1cf09107602d94cf35a388f0e87d2 100644 (file)
@@ -7,6 +7,7 @@ use crate::message::Diagnostic;
 
 use self::pivot::Value;
 
+pub mod cairo;
 pub mod csv;
 pub mod driver;
 pub mod page;
index f36cab04e25e694e91b665c40d7e242e2f273ad7..ea962068e2f41694854d878540a5b1d7116c3336 100644 (file)
@@ -74,3 +74,9 @@ impl Default for Setup {
         }
     }
 }
+
+impl Setup {
+    pub fn printable_size(&self) -> EnumMap<Axis2, f64> {
+        EnumMap::from_fn(|axis| self.paper[axis] - self.margins[axis][0] - self.margins[axis][1])
+    }
+}
index e6fbbb057fb6ba8c2ee467f25ce08d87849f7b8a..2c0a57755d1ac7654b48e58ba5a295e8d1b31fb5 100644 (file)
@@ -3,9 +3,12 @@ use std::{fmt::Debug, num::ParseFloatError, str::FromStr};
 use enum_map::enum_map;
 use serde::{de::Visitor, Deserialize};
 
-use crate::output::pivot::{
-    Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition,
-    FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, VertAlign,
+use crate::{
+    format::Decimal,
+    output::pivot::{
+        Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition,
+        FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, VertAlign,
+    },
 };
 use thiserror::Error as ThisError;
 
@@ -192,7 +195,7 @@ impl CellStyle {
                     TextAlignment::Center => Some(HorzAlign::Center),
                     TextAlignment::Decimal => Some(HorzAlign::Decimal {
                         offset: self.decimal_offset.as_px_f64(),
-                        c: '.',
+                        decimal: Decimal::Dot,
                     }),
                     TextAlignment::Mixed => None,
                 },
index 65905cced5e49fdc9d09dab2a4e8739908cd7259..b1b4a55b49fa623e518c4ab5248ffe3b9b00b16c 100644 (file)
@@ -53,7 +53,7 @@ use tlo::parse_tlo;
 
 use crate::{
     dictionary::Value as DataValue,
-    format::{Format, Settings as FormatSettings, Type, UncheckedFormat},
+    format::{Decimal, Format, Settings as FormatSettings, Type, UncheckedFormat},
     raw::VarType,
     settings::{Settings, Show},
 };
@@ -809,8 +809,8 @@ pub enum HorzAlign {
         /// Decimal offset from the right side of the cell, in 1/96" units.
         offset: f64,
 
-        /// Decimal character: either `b'.'` or `b','`.
-        c: char,
+        /// Decimal character.
+        decimal: Decimal,
     },
 }
 
@@ -842,7 +842,15 @@ pub struct FontStyle {
     pub underline: bool,
     pub markup: bool,
     pub font: String,
+
+    /// `fg[0]` is the usual foreground color.
+    ///
+    /// `fg[1]` is used only in [Area::Data] for odd-numbered rows.
     pub fg: [Color; 2],
+
+    /// `bg[0]` is the usual background color.
+    ///
+    /// `bg[1]` is used only in [Area::Data] for odd-numbered rows.
     pub bg: [Color; 2],
 
     /// In 1/72" units.
@@ -851,10 +859,10 @@ pub struct FontStyle {
 
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub struct Color {
-    alpha: u8,
-    r: u8,
-    g: u8,
-    b: u8,
+    pub alpha: u8,
+    pub r: u8,
+    pub g: u8,
+    pub b: u8,
 }
 
 impl Color {
@@ -1116,6 +1124,9 @@ impl Rect2 {
     pub fn translate(self, offset: Coord2) -> Rect2 {
         Self::from_fn(|axis| self[axis].start + offset[axis]..self[axis].end + offset[axis])
     }
+    pub fn is_empty(&self) -> bool {
+        self[Axis2::X].is_empty() || self[Axis2::Y].is_empty()
+    }
 }
 
 impl From<EnumMap<Axis2, Range<usize>>> for Rect2 {
@@ -1501,9 +1512,9 @@ impl PivotTable {
 #[derive(Clone, Debug)]
 pub struct Footnote {
     index: usize,
-    content: Box<Value>,
-    marker: Option<Box<Value>>,
-    show: bool,
+    pub content: Box<Value>,
+    pub marker: Option<Box<Value>>,
+    pub show: bool,
 }
 
 impl Footnote {
@@ -1680,6 +1691,17 @@ pub struct DisplayValue<'a> {
 }
 
 impl<'a> DisplayValue<'a> {
+    pub fn subscripts(&self) -> impl Iterator<Item = &str> {
+        self.subscripts.iter().map(String::as_str)
+    }
+
+    pub fn footnotes(&self) -> impl Iterator<Item = DisplayMarker<'_>> {
+        self.footnotes
+            .iter()
+            .filter(|f| f.show)
+            .map(|f| f.display_marker(self.options))
+    }
+
     pub fn without_suffixes(self) -> Self {
         Self {
             subscripts: &[],
@@ -1688,6 +1710,16 @@ impl<'a> DisplayValue<'a> {
         }
     }
 
+    /// Returns this display split into `(body, suffixes)` where `suffixes` is
+    /// subscripts and footnotes and `body` is everything else.
+    pub fn split_suffixes(self) -> (Self, Self) {
+        let suffixes = Self {
+            inner: &ValueInner::Empty,
+            ..self
+        };
+        (self.without_suffixes(), suffixes)
+    }
+
     pub fn with_styling(mut self, styling: &'a ValueStyle) -> Self {
         if let Some(area_style) = &styling.style {
             self.markup = area_style.font_style.markup;
@@ -1712,6 +1744,10 @@ impl<'a> DisplayValue<'a> {
         Self { footnotes, ..self }
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.inner.is_empty() && self.subscripts.is_empty() && self.footnotes.is_empty()
+    }
+
     fn small(&self) -> f64 {
         self.options.small
     }
index e0a72869c9bb663903a9cbabc82edfd4ef845211..9bf93f61956d5f7b01159b23273d698e18163a9d 100644 (file)
@@ -2,11 +2,11 @@ use std::sync::Arc;
 
 use enum_map::EnumMap;
 
-use crate::output::pivot::{
+use crate::output::{cairo::CairoDriver, pivot::{
     Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote, FootnoteMarkerPosition,
     FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition, Look, PivotTable,
     RowColBorder, Stroke,
-};
+}};
 
 use super::{Axis3, Value};
 
@@ -57,6 +57,11 @@ Columns
     );
 }
 
+fn d1_pdf() {
+    let pt = d1("Columns", Axis3::X);
+    let cairo = CairoDriver::new("d1.pdf");
+}
+
 #[test]
 fn d1_r() {
     assert_eq!(
index 61b58af0b8c3519ad4cd6519d9579119fd9c103a..85648c2adef596d14154fb45140268395d057399 100644 (file)
@@ -1,8 +1,11 @@
 use std::{fmt::Debug, io::Cursor};
 
-use crate::output::pivot::{
-    Axis2, Border, BoxBorder, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion,
-    LabelPosition, RowColBorder,
+use crate::{
+    format::Decimal,
+    output::pivot::{
+        Axis2, Border, BoxBorder, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion,
+        LabelPosition, RowColBorder,
+    },
 };
 
 use super::{Area, BorderStyle, Color, HorzAlign, Look, Stroke, VertAlign};
@@ -344,7 +347,7 @@ impl super::AreaStyle {
                     2 => Some(HorzAlign::Center),
                     4 => Some(HorzAlign::Decimal {
                         offset: style.decimal_offset as f64 / (72.0 * 20.0) * 96.0,
-                        c: '.',
+                        decimal: Decimal::Comma,
                     }),
                     _ => None,
                 },
index db2689c524c2f98d46483db146b62ba2f259d65f..7d9e80cb62d7ba06353cb8c0bf34ccd1a0b5afe4 100644 (file)
@@ -9,7 +9,7 @@ use itertools::interleave;
 use num::Integer;
 use smallvec::SmallVec;
 
-use crate::output::pivot::VertAlign;
+use crate::output::pivot::{DisplayValue, HorzAlign, VertAlign};
 
 use super::pivot::{
     AreaStyle, Axis2, BorderStyle, Coord2, Footnote, Look, PivotTable, Rect2, Stroke, ValueInner,
@@ -138,7 +138,7 @@ pub trait Device {
         &mut self,
         draw_cell: &DrawCell,
         alternate_row: bool,
-        bb: &Rect2,
+        bb: Rect2,
         valign_offset: usize,
         spill: EnumMap<Axis2, [usize; 2]>,
         clip: &Rect2,
@@ -183,6 +183,21 @@ impl<'a> DrawCell<'a> {
             value_options: &table.value_options,
         }
     }
+
+    pub fn display(&self) -> DisplayValue<'a> {
+        self.inner
+            .display(self.value_options)
+            .with_font_style(&self.style.font_style)
+            .with_subscripts(self.subscripts)
+            .with_footnotes(self.footnotes)
+    }
+
+    pub fn horz_align(&self, display: &DisplayValue) -> HorzAlign {
+        self.style
+            .cell_style
+            .horz_align
+            .unwrap_or_else(|| HorzAlign::for_mixed(display.var_type()))
+    }
 }
 
 /// A layout for rendering a specific table on a specific device.
@@ -993,7 +1008,7 @@ impl Page {
             VertAlign::Middle => self.extra_height(device, &bb, &draw_cell) / 2,
             VertAlign::Bottom => self.extra_height(device, &bb, &draw_cell),
         };
-        device.draw_cell(&draw_cell, alternate_row, &bb, valign_offset, spill, &clip)
+        device.draw_cell(&draw_cell, alternate_row, bb, valign_offset, spill, &clip)
     }
 }
 
index bd3455fed7ff4991baab9f74cf9aa5d41d600e03..1b35301c1c56c23fb8173338f29bac6ada5a3805 100644 (file)
@@ -15,7 +15,7 @@ use crate::output::{render::Extreme, text_line::Emphasis};
 
 use super::{
     driver::Driver,
-    pivot::{Axis2, BorderStyle, Coord2, DisplayValue, HorzAlign, PivotTable, Rect2, Stroke},
+    pivot::{Axis2, BorderStyle, Coord2, HorzAlign, PivotTable, Rect2, Stroke},
     render::{Device, DrawCell, Pager, Params},
     table::Content,
     text_line::{clip_text, TextLine},
@@ -330,14 +330,6 @@ impl TextRenderer {
         output
     }
 
-    fn display_cell<'a>(cell: &DrawCell<'a>) -> DisplayValue<'a> {
-        cell.inner
-            .display(cell.value_options)
-            .with_font_style(&cell.style.font_style)
-            .with_subscripts(cell.subscripts)
-            .with_footnotes(cell.footnotes)
-    }
-
     fn layout_cell(&self, text: &str, bb: Rect2) -> Coord2 {
         if text.is_empty() {
             return Coord2::default();
@@ -468,7 +460,7 @@ impl Device for TextRenderer {
     }
 
     fn measure_cell_width(&self, cell: &DrawCell) -> EnumMap<Extreme, usize> {
-        let text = Self::display_cell(cell).to_string();
+        let text = cell.display().to_string();
         enum_map![
             Extreme::Min => self.layout_cell(&text, Rect2::new(0..1, 0..usize::MAX)).x(),
             Extreme::Max => self.layout_cell(&text, Rect2::new(0..usize::MAX, 0..usize::MAX)).x(),
@@ -476,7 +468,7 @@ impl Device for TextRenderer {
     }
 
     fn measure_cell_height(&self, cell: &DrawCell, width: usize) -> usize {
-        let text = Self::display_cell(cell).to_string();
+        let text = cell.display().to_string();
         self.layout_cell(&text, Rect2::new(0..width, 0..usize::MAX))
             .y()
     }
@@ -509,18 +501,14 @@ impl Device for TextRenderer {
         &mut self,
         cell: &DrawCell,
         _alternate_row: bool,
-        bb: &Rect2,
+        bb: Rect2,
         valign_offset: usize,
         _spill: EnumMap<Axis2, [usize; 2]>,
         clip: &Rect2,
     ) {
-        let display = Self::display_cell(cell);
+        let display = cell.display();
         let text = display.to_string();
-        let horz_align = cell
-            .style
-            .cell_style
-            .horz_align
-            .unwrap_or_else(|| HorzAlign::for_mixed(display.var_type()));
+        let horz_align = cell.horz_align(&display);
 
         use Axis2::*;
         let breaks = new_line_breaks(&text, bb[X].len());