+++ /dev/null
-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");
- }
-}
--- /dev/null
+use std::{cmp::min, f64::consts::PI, fmt::Write, ops::DerefMut, sync::Arc};
+
+use cairo::{Context, 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::pivot::{Axis2, BorderStyle, Coord2, FontStyle, HorzAlign, Rect2, Stroke};
+use crate::output::render::{Device, DrawCell, Extreme, Params};
+use crate::output::{pivot::Color, table::Content};
+
+/// 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 CairoFsmStyle {
+ /// 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 CairoFsmStyle {
+ 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 CairoFsm {
+ style: Arc<CairoFsmStyle>,
+ params: Params,
+ context: Context,
+}
+
+impl CairoFsm {
+ pub fn new(style: Arc<CairoFsmStyle>, 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,
+ }
+ }
+
+ //pub fn render(&mut self, table: &PivotTable) {}
+}
+
+pub 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 CairoFsm {
+ 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 CairoFsm {
+ 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);
+ }
+}
--- /dev/null
+use std::{borrow::Cow, path::Path, sync::Arc};
+
+use cairo::PdfSurface;
+use enum_map::enum_map;
+use pango::SCALE;
+
+use crate::output::{
+ cairo::fsm::{parse_font_style, CairoFsm, CairoFsmStyle},
+ driver::Driver,
+ page::Setup,
+ pivot::{Color, Coord2, FontStyle},
+ Item,
+};
+
+use super::pivot::Axis2;
+
+pub mod fsm;
+
+pub struct CairoDriver {
+ renderer: CairoFsm,
+}
+
+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 = CairoFsmStyle {
+ 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 = CairoFsm::new(Arc::new(style), false, &surface);
+ Self { renderer }
+ }
+}
+
+impl Driver for CairoDriver {
+ fn name(&self) -> Cow<'static, str> {
+ Cow::from("cairo")
+ }
+
+ fn write(&mut self, item: &Arc<Item>) {
+ match &item.details {
+ super::Details::Chart => todo!(),
+ super::Details::Image => todo!(),
+ super::Details::Group(_vec) => todo!(),
+ super::Details::Message(_diagnostic) => todo!(),
+ super::Details::PageBreak => todo!(),
+ super::Details::Table(_pivot_table) => {
+ todo!()
+ }
+ super::Details::Text(_text) => todo!(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::output::cairo::CairoDriver;
+
+ #[test]
+ fn create() {
+ CairoDriver::new("test.pdf");
+ }
+}