use crate::Colormap;
use crate::PixelSink;
use crate::colorbar::render_colorbar_gradient;
use crate::colorbar::{ColorbarTicks, format_tick_label_with_units, format_units_label};
use crate::latex_render;
use crate::layout::ColorbarLayout;
use crate::plot::rasterize_to_surface;
use crate::render::RenderBackend;
use crate::render::target::{PixelSource, RenderTarget};
use crate::scale::generate_colorbar_ticks;
use cairo::{Context, Format, ImageSurface};
use std::f64::consts::PI;
pub struct PdfBackend<'a> {
cr: &'a Context,
width: f64,
height: f64,
}
impl<'a> PdfBackend<'a> {
pub fn new(cr: &'a Context, width: f64, height: f64) -> Self {
Self { cr, width, height }
}
}
impl<'a> RenderBackend for PdfBackend<'a> {
fn set_color(&mut self, r: u8, g: u8, b: u8, a: u8) {
self.cr.set_source_rgba(
r as f64 / 255.0,
g as f64 / 255.0,
b as f64 / 255.0,
a as f64 / 255.0,
);
}
fn fill_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
self.cr.rectangle(x, y, w, h);
self.cr.fill().unwrap();
}
fn stroke_line(&mut self, x0: f64, y0: f64, x1: f64, y1: f64, width: f64) {
self.cr.set_line_width(width);
self.cr.move_to(x0, y0);
self.cr.line_to(x1, y1);
self.cr.stroke().unwrap();
}
fn draw_text(&mut self, x: f64, y: f64, size: f64, text: &str) {
self.cr.set_font_size(size);
self.cr.set_font_face(
&cairo::FontFace::toy_create(
"STIXGeneral",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
)
.unwrap_or_else(|_| {
cairo::FontFace::toy_create(
"DejaVu Sans",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
)
.unwrap()
}),
);
self.cr.move_to(x, y);
self.cr.show_text(text).unwrap();
}
fn width(&self) -> f64 {
self.width
}
fn height(&self) -> f64 {
self.height
}
fn draw_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
self.cr.rectangle(x, y, w, h);
let _ = self.cr.fill();
}
fn draw_line(&mut self, x0: f64, y0: f64, x1: f64, y1: f64, width: f64) {
self.cr.set_line_width(width);
self.cr.move_to(x0, y0);
self.cr.line_to(x1, y1);
let _ = self.cr.stroke();
}
}
pub fn draw_projection_border_pdf(
cr: &Context,
map_x: f64,
map_y: f64,
map_width: f64,
map_height: f64,
line_width_px: f64,
) {
cr.set_source_rgb(0.0, 0.0, 0.0);
cr.set_line_width(line_width_px);
cr.set_antialias(cairo::Antialias::Best);
let n = 720;
let to_px = |x: f64| map_x + (x + 2.0) * 0.25 * map_width;
let to_py = |y: f64| map_y + (1.0 - y) * 0.5 * map_height;
for i in 0..=n {
let t = 2.0 * PI * (i as f64 / n as f64);
let x = 2.0 * t.cos();
let y = t.sin();
let px = to_px(x);
let py = to_py(y);
if i == 0 {
cr.move_to(px, py);
} else {
cr.line_to(px, py);
}
}
cr.close_path();
cr.stroke().unwrap();
}
fn draw_colorbar_pdf_ticks(cr: &Context, layout: &ColorbarLayout, ticks: &ColorbarTicks) {
let y0 = layout.y + layout.h;
let major_len = layout.major_tick_height;
let minor_len = layout.minor_tick_height;
cr.set_source_rgb(0.0, 0.0, 0.0);
let direction = match layout.tick_direction {
crate::cli::TickDirection::Inward => -1.0,
crate::cli::TickDirection::Outward => 1.0,
};
cr.set_line_width(layout.minor_tick_width);
for (&t, &_val) in ticks.minor_positions.iter().zip(ticks.minor_values.iter()) {
let x = t * (layout.w - 1.0) + layout.x;
cr.move_to(x, y0);
cr.line_to(x, y0 + direction * minor_len);
}
cr.set_line_width(layout.major_tick_width);
for (&t, &_val) in ticks.major_positions.iter().zip(ticks.major_values.iter()) {
let x = t * (layout.w - 1.0) + layout.x;
cr.move_to(x, y0);
cr.line_to(x, y0 + direction * major_len);
}
cr.stroke().unwrap();
}
fn draw_colorbar_pdf_labels(
cr: &Context,
layout: &ColorbarLayout,
ticks: &ColorbarTicks,
config: crate::params::ColorbarPdfLabelConfig,
) {
cr.set_source_rgb(0.0, 0.0, 0.0);
cr.select_font_face(
"Liberation Serif",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
);
let adjusted_tick_font_size = if let Some(w) = config.map_width {
(16.5 * 0.75) * (w / 300.0)
} else {
layout.tick_font_size
};
cr.set_font_size(adjusted_tick_font_size);
let base_label_y = layout.y + layout.h;
let tick_label_offset = match layout.tick_direction {
crate::cli::TickDirection::Outward => {
layout.major_tick_height + 0.5 * layout.major_tick_height
}
crate::cli::TickDirection::Inward => 1.0 * layout.major_tick_height,
};
let label_y = base_label_y + tick_label_offset;
for (&t, &val) in ticks.major_positions.iter().zip(ticks.major_values.iter()) {
let label = format_tick_label_with_units(
val,
config.scale,
Some(t),
config.latex_rendering,
config.units.as_deref(),
true,
);
let x = t * layout.w + layout.x;
if config.latex_rendering {
let math_label = format!("${}", label);
let math_label = format!("{}$", math_label);
let font_size_pt = (adjusted_tick_font_size * 1.25) as u32;
if let Some(rendered) = latex_render::render_latex_to_png(&math_label, font_size_pt) {
embed_latex_tick_label(cr, &rendered, x, label_y);
} else {
let ext = cr.text_extents(&label).unwrap();
let tx = x - ext.width() / 2.0;
cr.move_to(tx, label_y);
cr.show_text(&label).unwrap();
}
} else {
let ext = cr.text_extents(&label).unwrap();
let tx = x - ext.width() / 2.0;
cr.move_to(tx, label_y);
cr.show_text(&label).unwrap();
}
}
if let Some(units_str) = config.units.as_deref() {
let tick_offset = match layout.tick_direction {
crate::cli::TickDirection::Outward => layout.major_tick_height,
crate::cli::TickDirection::Inward => 0.0,
};
let spacing_scale = config.map_width.map(|w| w / 300.0).unwrap_or(1.0);
let vertical_gap = 12.0 * spacing_scale; let units_y_pos = label_y + tick_offset + vertical_gap;
let dpi_scale = config.map_width.map(|w| w / 300.0).unwrap_or(1.0);
let latex_font_size = ((config.units_font_size.unwrap_or(28.0) * 1.25) as u32).max(12);
let latex_dpi = ((300.0 * dpi_scale) as u32).max(100);
if let Some(rendered) =
latex_render::render_latex_to_hires_png(units_str, latex_font_size, latex_dpi)
{
embed_latex_png_in_colorbar(cr, &rendered, layout.x, layout.w, units_y_pos);
} else if let Some(units_label) = format_units_label(true, Some(units_str)) {
cr.select_font_face(
"Liberation Serif",
cairo::FontSlant::Normal,
cairo::FontWeight::Bold,
);
let font_size = config.units_font_size.unwrap_or(14.0) as f64;
cr.set_font_size(font_size);
let ext = cr.text_extents(&units_label).unwrap();
let center_x = layout.x + layout.w / 2.0 - ext.width() / 2.0;
cr.move_to(center_x, units_y_pos);
cr.show_text(&units_label).unwrap();
}
}
}
fn embed_latex_png_in_colorbar(
cr: &Context,
rendered: &latex_render::RenderedLatex,
colorbar_x: f64,
colorbar_width: f64,
y_pos: f64,
) {
if let Ok(img_buf) =
image::load_from_memory_with_format(&rendered.image_data, image::ImageFormat::Png)
{
let rgba = img_buf.to_rgba8();
if let Ok(mut surf) =
ImageSurface::create(Format::ARgb32, rgba.width() as i32, rgba.height() as i32)
{
let surf_stride = surf.stride() as usize;
{
let mut surf_data = surf.data().expect("Failed to get surface data");
let raw_data = rgba.as_raw();
for (i, chunk) in raw_data.chunks_exact(4).enumerate() {
let y = i / rgba.width() as usize;
let x = i % rgba.width() as usize;
let dst_idx = y * surf_stride + x * 4;
if dst_idx + 3 < surf_data.len() {
surf_data[dst_idx] = chunk[0]; surf_data[dst_idx + 1] = chunk[1]; surf_data[dst_idx + 2] = chunk[2]; surf_data[dst_idx + 3] = chunk[3]; }
}
}
surf.flush();
let mut min_y = rgba.height();
let mut max_y = 0u32;
for (_x, y, pixel) in rgba.enumerate_pixels() {
if pixel[3] > 10 {
min_y = min_y.min(y);
max_y = max_y.max(y);
}
}
let scale_factor = 0.25;
let scaled_width = (rgba.width() as f64) * scale_factor;
let scaled_height = (rgba.height() as f64) * scale_factor;
let center_x = colorbar_x + colorbar_width / 2.0 - scaled_width / 2.0;
let content_height = if min_y < max_y {
((max_y - min_y + 1) as f64) * scale_factor
} else {
scaled_height
};
let adjusted_y = y_pos - content_height * 0.75;
cr.save().unwrap();
cr.translate(center_x, adjusted_y);
cr.scale(scale_factor, scale_factor);
cr.set_source_surface(&surf, 0.0, 0.0).unwrap();
cr.paint().unwrap();
cr.restore().unwrap();
}
}
}
fn embed_latex_tick_label(
cr: &Context,
rendered: &latex_render::RenderedLatex,
x_pos: f64,
y_pos: f64,
) {
if let Ok(img_buf) =
image::load_from_memory_with_format(&rendered.image_data, image::ImageFormat::Png)
{
let rgba = img_buf.to_rgba8();
if let Ok(surf) = ImageSurface::create_for_data(
rgba.clone().into_raw(),
Format::ARgb32,
rendered.width as i32,
rendered.height as i32,
rendered.width as i32 * 4,
) {
let scale_factor = 1.0;
let display_width = rendered.width as f64 * scale_factor;
let display_height = rendered.height as f64 * scale_factor;
let x = x_pos - display_width / 2.0;
let y = y_pos - display_height / 2.0;
cr.save().unwrap();
cr.translate(x, y);
cr.scale(scale_factor, scale_factor);
cr.set_source_surface(&surf, 0.0, 0.0).unwrap();
cr.paint().unwrap();
cr.restore().unwrap();
}
}
}
pub fn draw_colorbar_pdf(
cr: &Context,
cb_layout: ColorbarLayout,
params: crate::params::ColorbarParams,
) {
let ticks = generate_colorbar_ticks(
params.minv,
params.maxv,
¶ms.scale_type,
params.hist_scale,
);
let surf =
ImageSurface::create(Format::ARgb32, cb_layout.w as i32, cb_layout.h as i32).unwrap();
let surf_cr = cairo::Context::new(&surf).unwrap();
surf_cr.set_operator(cairo::Operator::Source);
surf_cr.set_antialias(cairo::Antialias::None);
let surf = rasterize_to_surface(cb_layout.w as u32, cb_layout.h as u32, |sink| {
render_colorbar_gradient(
0,
0,
cb_layout.w as u32,
cb_layout.h as u32,
params.cmap,
params.gamma,
sink,
);
});
let _ = cr.set_source_surface(&surf, cb_layout.x, cb_layout.y);
cr.paint().unwrap();
draw_colorbar_pdf_ticks(cr, &cb_layout, &ticks);
draw_colorbar_pdf_labels(
cr,
&cb_layout,
&ticks,
crate::params::ColorbarPdfLabelConfig {
scale: params.scale_type,
latex_rendering: params.latex_rendering,
units: params.units.map(|s| s.to_string()),
units_font_size: params.units_font_size,
map_width: params.map_width,
},
);
draw_colorbar_pdf_extends(cr, &cb_layout, params.extend, params.cmap);
}
pub struct PdfRenderTarget<'a> {
pub cr: &'a cairo::Context,
}
impl RenderTarget for PdfRenderTarget<'_> {
fn blit_raster(&mut self, raster: &dyn PixelSource, x: f64, y: f64) {
let mut img_buffer =
crate::render::create_image_buffer_uninitialized(raster.width(), raster.height());
{
let mut sink = crate::PngSink {
img: &mut img_buffer,
x0: 0,
y0: 0,
};
for py in 0..raster.height() {
for px in 0..raster.width() {
let [r, g, b, a] = raster.get_pixel(px, py);
sink.draw_pixel(px, py, image::Rgba([r, g, b, a]));
}
}
}
if let Ok(surface) = ImageSurface::create_for_data(
img_buffer.clone().into_raw(),
cairo::Format::ARgb32,
raster.width() as i32,
raster.height() as i32,
raster.width() as i32 * 4,
) {
let _ = self.cr.set_source_surface(&surface, x, y);
self.cr.paint().unwrap();
}
}
}
fn draw_colorbar_pdf_extends(
cr: &Context,
layout: &ColorbarLayout,
extend: &crate::cli::Extend,
cmap: &Colormap,
) {
use crate::cli::Extend;
if extend == &Extend::None {
return;
}
let min_color_rgb = cmap.sample(0.0);
let max_color_rgb = cmap.sample(1.0);
let tip_distance = layout.h * 0.5;
cr.set_line_width(0.5);
if matches!(extend, Extend::Min | Extend::Both) {
let tip_x = layout.x - tip_distance;
let base_x = layout.x;
let base_top_y = layout.y;
let base_bottom_y = layout.y + layout.h;
let tip_y = layout.y + layout.h / 2.0;
cr.set_source_rgb(
min_color_rgb.0[0] as f64 / 255.0,
min_color_rgb.0[1] as f64 / 255.0,
min_color_rgb.0[2] as f64 / 255.0,
);
cr.move_to(tip_x, tip_y);
cr.line_to(base_x, base_top_y);
cr.line_to(base_x, base_bottom_y);
cr.close_path();
cr.fill().unwrap();
}
if matches!(extend, Extend::Max | Extend::Both) {
let tip_x = layout.x + layout.w + tip_distance;
let base_x = layout.x + layout.w;
let base_top_y = layout.y;
let base_bottom_y = layout.y + layout.h;
let tip_y = layout.y + layout.h / 2.0;
cr.set_source_rgb(
max_color_rgb.0[0] as f64 / 255.0,
max_color_rgb.0[1] as f64 / 255.0,
max_color_rgb.0[2] as f64 / 255.0,
);
cr.move_to(tip_x, tip_y);
cr.line_to(base_x, base_top_y);
cr.line_to(base_x, base_bottom_y);
cr.close_path();
cr.fill().unwrap();
}
}
pub fn draw_figure_labels_pdf(
cr: &Context,
width: f64,
height: f64,
rlabel: &Option<String>,
llabel: &Option<String>,
latex_rendering: bool,
label_font_size: Option<f32>,
) {
let scale = width / 800.0;
let font_size = if let Some(size) = label_font_size {
size as f64 * scale
} else {
14.0 * scale + 2.0
};
let font_size_pt = if let Some(size) = label_font_size {
(size as u32).max(6)
} else {
((14.0 * scale as f32 + 2.0) as u32).max(6)
};
cr.set_source_rgb(0.0, 0.0, 0.0);
cr.set_font_size(font_size);
cr.set_font_face(
&cairo::FontFace::toy_create(
"STIXGeneral",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
)
.unwrap_or_else(|_| {
cairo::FontFace::toy_create(
"DejaVu Sans",
cairo::FontSlant::Normal,
cairo::FontWeight::Normal,
)
.unwrap()
}),
);
let padding_x = 20.0 * scale; let x_left = padding_x;
let x_right = width - padding_x;
let y_label = padding_x + (height * 0.095);
if let Some(text) = llabel {
if latex_rendering {
if let Some(rendered) = latex_render::render_latex_to_hires_png(text, font_size_pt, 150)
{
embed_latex_png_in_label(cr, &rendered, x_left, y_label, false);
} else if let Some(rendered) = latex_render::render_latex_to_png(text, font_size_pt) {
embed_latex_png_in_label(cr, &rendered, x_left, y_label, false);
} else {
cr.move_to(x_left, y_label);
cr.show_text(text).unwrap();
}
} else {
cr.move_to(x_left, y_label);
cr.show_text(text).unwrap();
}
}
if let Some(text) = rlabel {
if latex_rendering {
if let Some(rendered) = latex_render::render_latex_to_hires_png(text, font_size_pt, 150)
{
embed_latex_png_in_label(cr, &rendered, x_right, y_label, true);
} else if let Some(rendered) = latex_render::render_latex_to_png(text, font_size_pt) {
embed_latex_png_in_label(cr, &rendered, x_right, y_label, true);
} else {
let extents = cr.text_extents(text).unwrap();
let x = x_right - extents.x_advance();
cr.move_to(x, y_label);
cr.show_text(text).unwrap();
}
} else {
let extents = cr.text_extents(text).unwrap();
let x = x_right - extents.x_advance();
cr.move_to(x, y_label);
cr.show_text(text).unwrap();
}
}
}
fn embed_latex_png_in_label(
cr: &Context,
rendered: &latex_render::RenderedLatex,
x_pos: f64,
y_pos: f64,
right_aligned: bool,
) {
if let Ok(img_buf) =
image::load_from_memory_with_format(&rendered.image_data, image::ImageFormat::Png)
{
let rgba = img_buf.to_rgba8();
if let Ok(surf) = ImageSurface::create_for_data(
rgba.clone().into_raw(),
Format::ARgb32,
rendered.width as i32,
rendered.height as i32,
rendered.width as i32 * 4,
) {
let scale_factor = 1.0;
let display_width = rendered.width as f64 * scale_factor;
let display_height = rendered.height as f64 * scale_factor;
let x = if right_aligned {
x_pos - display_width
} else {
x_pos
};
cr.save().unwrap();
cr.translate(x, y_pos - display_height / 2.0);
cr.scale(scale_factor, scale_factor);
cr.set_source_surface(&surf, 0.0, 0.0).unwrap();
cr.paint().unwrap();
cr.restore().unwrap();
}
}
}