collections::HashMap,
fmt::{Display, Write},
iter::{once, repeat},
- ops::{Index, Not, Range},
+ ops::{Index, IndexMut, Not, Range, RangeInclusive},
str::from_utf8,
sync::{Arc, OnceLock, Weak},
};
/// arbitrary. The ultimate reason for the division is simply because that's
/// how SPSS documentation and file formats do it.
#[derive(Clone, Debug)]
-struct Look {
- name: Option<String>,
+pub struct Look {
+ pub name: Option<String>,
- omit_empty: bool,
- row_labels_in_corner: bool,
+ pub omit_empty: bool,
+ pub row_labels_in_corner: bool,
- /// Range of column widths for columns in the row headings and corner , in 1/96"
- /// units.
- row_heading_widths: Range<usize>,
-
- /// Range of column widths for columns in the column headings , in 1/96"
- /// units.
- col_heading_widths: Range<usize>,
+ /// Ranges of column widths in the two heading regions, in 1/96" units.
+ pub heading_widths: EnumMap<HeadingRegion, RangeInclusive<usize>>,
/// Kind of markers to use for footnotes.
- footnote_marker_type: FootnoteMarkerType,
+ pub footnote_marker_type: FootnoteMarkerType,
/// Where to put the footnote markers.
- footnote_marker_position: FootnoteMarkerPosition,
+ pub footnote_marker_position: FootnoteMarkerPosition,
/// Styles for areas of the pivot table.
- areas: EnumMap<Area, AreaStyle>,
+ pub areas: EnumMap<Area, AreaStyle>,
/// Styles for borders in the pivot table.
- borders: EnumMap<Border, BorderStyle>,
+ pub borders: EnumMap<Border, BorderStyle>,
- print_all_layers: bool,
+ pub print_all_layers: bool,
- paginate_layers: bool,
+ pub paginate_layers: bool,
- shrink_to_fit: EnumMap<Axis2, bool>,
+ pub shrink_to_fit: EnumMap<Axis2, bool>,
- top_continuation: bool,
+ pub top_continuation: bool,
- bottom_continuation: bool,
+ pub bottom_continuation: bool,
- continuation: Option<String>,
+ pub continuation: Option<String>,
- n_orphan_lines: usize,
+ pub n_orphan_lines: usize,
}
impl Default for Look {
name: None,
omit_empty: true,
row_labels_in_corner: true,
- row_heading_widths: 36..72,
- col_heading_widths: 36..120,
+ heading_widths: EnumMap::from_fn(|region| match region {
+ HeadingRegion::RowHeadings => 36..=72,
+ HeadingRegion::ColumnHeadings => 36..=120,
+ }),
footnote_marker_type: FootnoteMarkerType::default(),
footnote_marker_position: FootnoteMarkerPosition::default(),
areas: EnumMap::from_fn(Area::default_area_style),
}
}
+/// The heading region of a rendered pivot table:
+///
+/// ```text
+/// +------------------+-------------------------------------------------+
+/// | | column headings |
+/// | +-------------------------------------------------+
+/// | corner | |
+/// | and | |
+/// | row headings | data |
+/// | | |
+/// | | |
+/// +------------------+-------------------------------------------------+
+/// ```
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum)]
+pub enum HeadingRegion {
+ RowHeadings,
+ ColumnHeadings,
+}
+
#[derive(Clone, Debug)]
pub struct AreaStyle {
cell_style: CellStyle,
#[derive(Copy, Clone, Debug)]
pub struct BorderStyle {
- stroke: Stroke,
- color: Color,
+ pub stroke: Stroke,
+ pub color: Color,
}
-#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Enum)]
pub enum Stroke {
None,
Solid,
})
}
+ pub fn for_axis((a, az): (Axis2, usize), bz: usize) -> Self {
+ let mut coord = Self::default();
+ coord[a] = az;
+ coord[!a] = bz;
+ coord
+ }
+
pub fn x(&self) -> usize {
self.0[Axis2::X]
}
}
}
+impl IndexMut<Axis2> for Coord2 {
+ fn index_mut(&mut self, index: Axis2) -> &mut Self::Output {
+ &mut self.0[index]
+ }
+}
+
#[derive(Clone, Debug, Default)]
pub struct Rect2(pub EnumMap<Axis2, Range<usize>>);
/// `axes[Axis3::Z].dimensions[i].data_leaves[]`, except that a dimension
/// can have zero leaves, in which case `current_layer[i]` is zero and
/// there's no corresponding leaf.
- current_layer: Vec<usize>,
+ pub current_layer: Vec<usize>,
/// Column and row sizing and page breaks.
sizing: EnumMap<Axis2, Sizing>,
--- /dev/null
+use std::collections::HashMap;
+use std::ops::RangeInclusive;
+use std::sync::Arc;
+
+use enum_map::EnumMap;
+use smallvec::SmallVec;
+
+use super::pivot::{Axis2, BorderStyle, Coord2, HeadingRegion, Look, PivotTable, Rect2, Stroke};
+use super::table::{Cell, CellInner, Table};
+
+/// Parameters for rendering a table_item to a device.
+///
+///
+/// # Coordinate system
+///
+/// The rendering code assumes that larger `x` is to the right and larger `y`
+/// toward the bottom of the page.
+///
+/// The rendering code assumes that the table being rendered has its upper left
+/// corner at (0,0) in device coordinates. This is usually not the case from
+/// the driver's perspective, so the driver should expect to apply its own
+/// offset to coordinates passed to callback functions.
+pub struct Params {
+ /// Page size to try to fit the rendering into. Some tables will, of
+ /// course, overflow this size.
+ size: Coord2,
+
+ /// Nominal size of a character in the most common font:
+ /// `font_size[Axis2::X]` is the em width.
+ /// `font_size[Axis2::Y]` is the line leading.
+ font_size: EnumMap<Axis2, usize>,
+
+ /// Width of different kinds of lines.
+ line_widths: EnumMap<Stroke, usize>,
+
+ /// 1/96" of an inch (1px) in the rendering unit. Currently used only for
+ /// column width ranges, as in `width_ranges` in
+ /// [crate::output::pivot::Look]. Set to `None` to disable this feature.
+ px_size: Option<usize>,
+
+ /// Minimum cell width or height before allowing the cell to be broken
+ /// across two pages. (Joined cells may always be broken at join
+ /// points.)
+ min_break: EnumMap<Axis2, usize>,
+
+ /// True if the driver supports cell margins. (If false, the rendering
+ /// engine will insert a small space betweeen adjacent cells that don't have
+ /// an intervening rule.)
+ supports_margins: bool,
+
+ /// True if the local language has a right-to-left direction, otherwise
+ /// false.
+ rtl: bool,
+
+ /// True if the table is being rendered for printing (as opposed to
+ /// on-screen display).
+ printing: bool,
+
+ /// Whether [RenderOps::adjust_break] is implemented.
+ can_adjust_break: bool,
+
+ /// Whether [RenderOps::scale] is implemented.
+ can_scale: bool,
+}
+
+pub trait Device {
+ fn params(&self) -> &Params;
+
+ /// Measures `cell`'s width. Returns an arary `[min_width, max_width]`,
+ /// where `min_width` is the minimum width required to avoid splitting a
+ /// single word across multiple lines (normally, this is the width of the
+ /// longest word in the cell) and `max_width` is the minimum width required
+ /// to avoid line breaks other than at new-lines.
+ fn measure_cell_width(&self, cell: &CellInner) -> [usize; 2];
+
+ /// Returns the height required to render `cell` given a width of `width`.
+ fn measure_cell_height(&self, cell: &CellInner, width: usize) -> usize;
+
+ /// Given that there is space measuring `width` by `height` to render
+ /// `cell`, where `height` is insufficient to render the entire height of
+ /// the cell, returns the largest height less than `height` at which it is
+ /// appropriate to break the cell. For example, if breaking at the
+ /// specified `height` would break in the middle of a line of text, the
+ /// return value would be just sufficiently less that the breakpoint would
+ /// be between lines of text.
+ ///
+ /// Optional. If [RenderParams::can_adjust_break] is false, the rendering
+ /// engine assumes that all breakpoints are acceptable.
+ fn adjust_break(&self, cell: &Cell, size: Coord2) -> usize;
+
+ /// Draws a generalized intersection of lines in `bb`.
+ ///
+ /// `styles` is interpreted this way:
+ ///
+ /// `styles[Axis2::X][0]`: style of line from top of `bb` to its center.
+ /// `styles[Axis2::X][1]`: style of line from bottom of `bb` to its center.
+ /// `styles[Axis2::Y][0]`: style of line from left of `bb` to its center.
+ /// `styles[Axis2::Y][1]`: style of line from right of `bb` to its center.
+ fn draw_line(&mut self, bb: Rect2, styles: EnumMap<Axis2, [BorderStyle; 2]>);
+
+ /// Draws `cell` within bounding box `bb`. `clip` is the same as `bb` (the
+ /// common case) or a subregion enclosed by `bb`. In the latter case only
+ /// the part of the cell that lies within `clip` should actually be drawn,
+ /// although `bb` should used to determine the layout of the cell.
+ ///
+ /// The text in the cell needs to be vertically offset `valign_offset` units
+ /// from the top of the bounding box. This handles vertical alignment with
+ /// the cell. (The caller doesn't just reduce the bounding box size because
+ /// that would prevent the implementation from filling the entire cell with
+ /// the background color.) The implementation must handle horizontal
+ /// alignment itself.
+ fn draw_cell(
+ &mut self,
+ cell: &Cell,
+ color_idx: usize,
+ bb: &Rect2,
+ valign_offset: usize,
+ spill: EnumMap<Axis2, [usize; 2]>,
+ clip: &Rect2,
+ );
+
+ /// Scales all output by `factor`, e.g. a `factor` of 0.5 would cause
+ /// everything subsequent to be drawn half-size. `factor` will be greater
+ /// than 0 and less than or equal to 1.
+ ///
+ /// Optional. If [RenderParams::can_scale] is false, the rendering engine
+ /// won't try to scale output.
+ fn scale(&mut self, factor: f64);
+}
+
+/// A layout for rendering a specific table on a specific device.
+///
+/// May represent the layout of an entire table presented to [Pager::new], or a
+/// rectangular subregion of a table broken out using [Break::next] to allow a
+/// table to be broken across multiple pages.
+///
+/// A page's size is not limited to the size passed in as part of [Params].
+/// [Pager] breaks a [Page] into smaller [page]s that will fit in the available
+/// space.
+struct Page {
+ params: Arc<Params>,
+ table: Arc<Table>,
+
+ /// Region of `table` to render.
+ ///
+ /// The horizontal cells rendered are the leftmost h[H][0], then
+ /// r[H][0] through r[H][1], exclusive, then the rightmost h[H][1].
+ ///
+ /// The vertical cells rendered are the topmost h[V][0], then r[V][0]
+ /// through r[V][1], exclusive, then the bottommost h[V][1].
+ ///
+ /// n[H] = h[H][0] + (r[H][1] - r[H][0]) + h[H][1]
+ /// n[V] = h[V][0] + (r[V][1] - r[V][0]) + h[V][1]
+ n: Coord2,
+ /*
+ int h[TABLE_N_AXES][2];
+ int r[TABLE_N_AXES][2];
+ int n[TABLE_N_AXES];*/
+ /// "Cell positions".
+ ///
+ /// cp[H] represents x positions within the table.
+ /// cp[H][0] = 0.
+ /// cp[H][1] = the width of the leftmost vertical rule.
+ /// cp[H][2] = cp[H][1] + the width of the leftmost column.
+ /// cp[H][3] = cp[H][2] + the width of the second-from-left vertical rule.
+ /// and so on:
+ /// cp[H][2 * n[H]] = x position of the rightmost vertical rule.
+ /// cp[H][2 * n[H] + 1] = total table width including all rules.
+ ///
+ /// Similarly, cp[V] represents y positions within the table.
+ /// cp[V][0] = 0.
+ /// cp[V][1] = the height of the topmost horizontal rule.
+ /// cp[V][2] = cp[V][1] + the height of the topmost row.
+ /// cp[V][3] = cp[V][2] + the height of the second-from-top horizontal rule.
+ /// and so on:
+ /// cp[V][2 * n[V]] = y position of the bottommost horizontal rule.
+ /// cp[V][2 * n[V] + 1] = total table height including all rules.
+ ///
+ /// Rules and columns can have width or height 0, in which case consecutive
+ /// values in this array are equal.
+ cp: EnumMap<Axis2, Vec<usize>>,
+
+ /// [Break::next] can break a table such that some cells are not fully
+ /// contained within a render_page. This will happen if a cell is too wide
+ /// or two tall to fit on a single page, or if a cell spans multiple rows
+ /// or columns and the page only includes some of those rows or columns.
+ ///
+ /// This hash table contains represents each such cell that doesn't
+ /// completely fit on this page.
+ ///
+ /// Each overflow cell borders at least one header edge of the table and may
+ /// border more. (A single table cell that is so large that it fills the
+ /// entire page can overflow on all four sides!)
+ ///
+ /// # Interpretation
+ ///
+ /// overflow[H][0]: space trimmed off its left side.
+ /// overflow[H][1]: space trimmed off its right side.
+ /// overflow[V][0]: space trimmed off its top.
+ /// overflow[V][1]: space trimmed off its bottom.
+ ///
+ /// During rendering, this information is used to position the rendered
+ /// portion of the cell within the available space.
+ ///
+ /// When a cell is rendered, sometimes it is permitted to spill over into
+ /// space that is ordinarily reserved for rules. Either way, this space is
+ /// still included in overflow values.
+ ///
+ /// Suppose, for example, that a cell that joins 2 columns has a width of 60
+ /// pixels and content "abcdef", that the 2 columns that it joins have
+ /// widths of 20 and 30 pixels, respectively, and that therefore the rule
+ /// between the two joined columns has a width of 10 (20 + 10 + 30 = 60).
+ /// It might render like this, if each character is 10x10, and showing a few
+ /// extra table cells for context:
+ ///
+ /// ```text
+ /// +------+
+ /// |abcdef|
+ /// +--+---+
+ /// |gh|ijk|
+ /// +--+---+
+ /// ```
+ ///
+ /// If this render_page is broken at the rule that separates "gh" from
+ /// "ijk", then the page that contains the left side of the "abcdef" cell
+ /// will have overflow[H][1] of 10 + 30 = 40 for its portion of the cell,
+ /// and the page that contains the right side of the cell will have
+ /// overflow[H][0] of 20 + 10 = 30. The two resulting pages would look like
+ /// this:
+ ///
+ /// ```text
+ /// +---
+ /// |abc
+ /// +--+
+ /// |gh|
+ /// +--+
+ /// ```
+ ///
+ /// and:
+ ///
+ /// ```text
+ /// ----+
+ /// cdef|
+ /// +---+
+ /// |ijk|
+ /// +---+
+ /// ```
+ /// Each entry maps from a cell that overflows to the space that has been
+ /// trimmed off the cell:
+ overflows: HashMap<Coord2, EnumMap<Axis2, [usize; 2]>>,
+
+ /// If a single column (or row) is too wide (or tall) to fit on a page
+ /// reasonably, then render_break_next() will split a single row or column
+ /// across multiple render_pages. This member indicates when this has
+ /// happened:
+ ///
+ /// is_edge_cutoff[H][0] is true if pixels have been cut off the left side
+ /// of the leftmost column in this page, and false otherwise.
+ ///
+ /// is_edge_cutoff[H][1] is true if pixels have been cut off the right side
+ /// of the rightmost column in this page, and false otherwise.
+ ///
+ /// is_edge_cutoff[V][0] and is_edge_cutoff[V][1] are similar for the top
+ /// and bottom of the table.
+ ///
+ /// The effect of is_edge_cutoff is to prevent rules along the edge in
+ /// question from being rendered.
+ ///
+ /// When is_edge_cutoff is true for a given edge, the 'overflows' hmap will
+ /// contain a node for each cell along that edge.
+ is_edge_cutoff: EnumMap<Axis2, [bool; 2]>,
+
+ /// If part of a joined cell would be cut off by breaking a table along
+ /// 'axis' at the rule with offset 'z' (where 0 <= z <= n[axis]), then
+ /// join_crossing[axis][z] is the thickness of the rule that would be cut
+ /// off.
+ ///
+ /// This is used to know to allocate extra space for breaking at such a
+ /// position, so that part of the cell's content is not lost.
+ ///
+ /// This affects breaking a table only when headers are present. When
+ /// headers are not present, the rule's thickness is used for cell content,
+ /// so no part of the cell's content is lost (and in fact it is duplicated
+ /// across both pages).
+ join_crossing: EnumMap<Axis2, usize>,
+
+ /// Minimum and maximum widths of columns based on their headings.
+ width_ranges: EnumMap<HeadingRegion, RangeInclusive<usize>>,
+}
+
+impl Page {
+ /// Creates and returns a new [Page] for rendering `table` with the given
+ /// `look` on `device`.
+ ///
+ /// The new [Page] will be suitable for rendering on a device whose page
+ /// size is `params.size`, but the caller is responsible for actually
+ /// breaking it up to fit on such a device, using the [Break] abstraction.
+ fn new(table: &Table, device: &dyn Device, min_width: usize, look: &Look) -> Self {
+ // Figure out rule widths.
+ let rules = EnumMap::from_fn(|axis| {
+ let n = table.n[axis];
+ (0..n)
+ .map(|z| measure_rule(table, device, axis, z))
+ .collect::<Vec<_>>()
+ });
+
+ let px_size = device.params().px_size.unwrap_or_default();
+ let heading_widths = look
+ .heading_widths
+ .clone()
+ .map(|region, range| [*range.start() * px_size, *range.end() * px_size]);
+
+ // Calculate minimum and maximum widths of cells that do not span
+ // multiple columns.
+ let mut columns = [vec![0; table.n.x()], vec![0; table.n.x()]];
+ for y in 0..table.n[Axis2::Y] {
+ for x in table.iter_x(y) {
+ let coord = Coord2::new(x, y);
+ let contents = table.get(coord);
+ if !contents.is_top_left(coord) || contents.col_span() != 1 {
+ continue;
+ }
+
+ let mut w = device.measure_cell_width(contents.inner());
+ if let Some(px_size) = device.params().px_size {
+ if let Some(region) = table.heading_region(coord) {
+ let wr = &heading_widths[region];
+ if w[0] < wr[0] {
+ w[0] = wr[0];
+ if w[0] > w[1] {
+ w[1] = w[0];
+ }
+ } else if w[1] > wr[1] {
+ w[1] = wr[1];
+ if w[1] < w[0] {
+ w[0] = w[1];
+ }
+ }
+ }
+ }
+
+ for i in 0..2 {
+ if columns[i][x] < w[i] {
+ columns[i][x] = w[i];
+ }
+ }
+ }
+ }
+
+ // Distribute widths of spanned columns.
+ let columns = columns.map(|widths| {
+ widths
+ .into_iter()
+ .map(|width| Row {
+ unspanned: width,
+ width,
+ })
+ .collect::<Vec<_>>()
+ });
+ for y in 0..table.n[Axis2::Y] {
+ for x in table.iter_x(y) {
+ let coord = Coord2::new(x, y);
+ let contents = table.get(coord);
+ if !contents.is_top_left(coord) || contents.col_span() == 1 {
+ continue;
+ }
+
+ let w = device.measure_cell_width(contents.inner());
+ for i in 0..2 {
+ //distribute_spanned_width(w[i],
+ }
+ }
+ }
+
+ todo!()
+ }
+}
+
+/// Row or column dimension.
+#[derive(Clone, Default)]
+struct Row {
+ /// Width without considering rows (or columns) that span more than one row
+ /// (or column).
+ unspanned: usize,
+
+ /// Width take spanned rows (or columns) into consideration.
+ width: usize,
+}
+
+/// Returns the width of the rule in `table` that is at offset `z` along axis
+/// `a`, if rendered on `device`.
+fn measure_rule(table: &Table, device: &dyn Device, a: Axis2, z: usize) -> usize {
+ let b = !a;
+
+ // Determine the types of rules that are present.
+ let mut rules = EnumMap::default();
+ for w in 0..table.n[b] {
+ let stroke = table.get_rule(a, Coord2::for_axis((a, z), w)).stroke;
+ rules[stroke] = true;
+ }
+
+ // Turn off [Stroke::None] because it has width 0 and we needn't bother.
+ // However, if the device doesn't support margins, make sure that there is
+ // at least a small gap between cells (but we don't need any at the left or
+ // right edge of the table).
+ if rules[Stroke::None] {
+ rules[Stroke::None] = false;
+ if z > 0 && z < table.n[a] && !device.params().supports_margins && a == Axis2::X {
+ rules[Stroke::Solid] = true;
+ }
+ }
+
+ // Calculate maximum width of rules that are present.
+ let line_widths = &device.params().line_widths;
+ rules
+ .into_iter()
+ .map(
+ |(rule, present)| {
+ if present {
+ line_widths[rule]
+ } else {
+ 0
+ }
+ },
+ )
+ .max()
+ .unwrap_or(0)
+}
+
+struct Break {
+ page: Arc<Page>,
+
+ /// Axis along which `page` is being broken.
+ axis: Axis2,
+
+ /// Next cell along `axis`.
+ z: usize,
+
+ /// Pixel offset within cell `z` (usually 0).
+ pixel: usize,
+
+ /// Width of headers of `page` along `axis`.
+ hw: usize,
+}
+
+pub struct Pager {
+ device: Box<dyn Device>,
+ scale: f64,
+
+ /// [Page]s to be rendered, in order, vertically. There may be up to 5
+ /// pages, for the pivot table's title, layers, body, captions, and
+ /// footnotes.
+ pages: SmallVec<[Box<Page>; 5]>,
+
+ cur_page: usize,
+ x_break: Break,
+ y_break: Break,
+}
+
+impl Pager {
+ pub fn new(
+ device: Box<dyn Device>,
+ pivot_table: &PivotTable,
+ layer_indexes: Option<&[usize]>,
+ ) -> Self {
+ let output = pivot_table.output(
+ layer_indexes.unwrap_or(&pivot_table.current_layer),
+ device.params().printing,
+ );
+
+ todo!()
+ }
+}