Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 91 additions & 12 deletions ggsql-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ pub struct RenderArgs {
pub verbose: bool,
}

/// The flags `view` takes: where the data comes from and how the window looks.
/// The flags `view` takes: where the data comes from, how the window looks,
/// and — optionally — where the plot goes once the window closes.
///
/// Deliberately not [`RenderArgs`]: there is no `--writer` to choose and no
/// `--output` to write, and `-D` carries the viewer's own settings rather than
/// a writer's. The reader flags are the same ones, so they come from the same
/// [`ReaderArgs`].
/// Shares the reader flags with [`RenderArgs`] through [`ReaderArgs`], and its
/// `--writer`/`--output` mean the same thing they do there, but `-D` is the
/// viewer's own: the window's settings (`width`, `height`, `background`,
/// `title`) are taken from it, and any other key is forwarded to the writer
/// for the after-close render.
#[derive(Args)]
pub struct ViewArgs {
#[command(flatten)]
Expand All @@ -104,11 +106,28 @@ pub struct ViewArgs {
value_name = "KEY=VALUE[;...]",
long_help = "Settings for the viewer window, as `key=value`. Repeatable, and one flag \
may carry several settings separated by `;` (quote it, as most shells read \
`;` themselves): `-D 'width=1280;title=My plot'`.\n\nSettings:\n \
width, height, background, title"
`;` themselves): `-D 'width=1280;title=My plot'`.\n\nViewer settings:\n \
width, height, background, title\n\nAny other key (e.g. `compression`) is \
forwarded to the writer when `--output` or `--writer` is given, except `dpi` \
and `units`, which are refused: the render's size and resolution always come \
from the window when it closed, so what you see is what you get."
)]
pub viewer_options: Vec<String>,

/// Output format for a render after the window closes
///
/// With `--output` or on its own (writing to stdout), renders the plot
/// once the window closes, sized to the window as the user left it.
#[arg(short, long, long_help = writers::writer_help())]
pub writer: Option<String>,

/// Render the plot to this file after the window closes
///
/// The render uses the window's dimensions at close, so the file matches
/// what was last on screen.
#[arg(short, long)]
pub output: Option<PathBuf>,

/// Show verbose output (execution details, statistics)
#[arg(short, long)]
pub verbose: bool,
Expand Down Expand Up @@ -193,7 +212,9 @@ pub enum Commands {
/// Show a ggsql query's plot in a window
///
/// Blocks until the window is closed. Resizing the window re-lays-out the
/// plot rather than stretching it.
/// plot rather than stretching it. With `--output` (and optionally
/// `--writer`), the plot is rendered to file when the window closes, at
/// the window's final dimensions, which are also logged to the terminal.
///
/// Requires the `window` feature and a working GPU adapter.
View {
Expand Down Expand Up @@ -486,9 +507,42 @@ fn render_spec(spec: Spec, args: &RenderArgs, writer: &WriterSpec) {
fn cmd_view(query: String, args: &ViewArgs) {
#[cfg(feature = "window")]
{
use ggsql::writer::PlotViewer;
use ggsql::writer::{PlotViewer, VIEWER_OPTIONS};

// `-D` serves two masters: the viewer's own keys configure the window,
// everything else is meant for the writer of the after-close render.
// `dpi` and `units` belong to neither: the render must match what was
// on screen, and both would reinterpret the window's size.
let mut viewer_pairs: Vec<String> = Vec::new();
let mut writer_pairs: Vec<String> = Vec::new();
for raw in &args.viewer_options {
for setting in raw.split(';').map(str::trim).filter(|s| !s.is_empty()) {
let key = setting.split('=').next().unwrap_or("").trim();
if matches!(key, "dpi" | "units") {
eprintln!(
"view takes no '{key}' setting: the render uses the window's own size and \
resolution, so what you see is what you get"
);
std::process::exit(1);
}
if VIEWER_OPTIONS.contains(&key) {
viewer_pairs.push(setting.to_string());
} else {
writer_pairs.push(setting.to_string());
}
}
}

let render_requested = args.writer.is_some() || args.output.is_some();
if !writer_pairs.is_empty() && !render_requested {
eprintln!(
"Settings like '{}' are writer settings; pass --output or --writer to use them",
writer_pairs[0]
);
std::process::exit(1);
}

let options = WriterOptions::parse(args.viewer_options.clone()).unwrap_or_else(|e| {
let options = WriterOptions::parse(viewer_pairs).unwrap_or_else(|e| {
eprintln!("{}", e);
std::process::exit(1);
});
Expand Down Expand Up @@ -531,10 +585,35 @@ fn cmd_view(query: String, args: &ViewArgs) {
eprintln!("Close the window to exit.");
}

// Blocks on the main thread until the window closes.
if let Err(e) = viewer.show(&spec) {
// Blocks on the main thread until the window closes, returning the
// frame (size and dpi) the user left the window at.
let frame = viewer.show(&spec).unwrap_or_else(|e| {
eprintln!("{}", e);
std::process::exit(1);
});
eprintln!(
"Window closed at {}x{} (dpi: {})",
frame.width, frame.height, frame.dpi
);

if render_requested {
// The window's closing frame wins over the writer's defaults: the
// file should match what was on screen, chrome included.
writer_pairs.push(format!("width={}", frame.width));
writer_pairs.push(format!("height={}", frame.height));
writer_pairs.push(format!("dpi={}", frame.dpi));
let render_args = RenderArgs {
source: ReaderArgs {
reader: args.source.reader.clone(),
cache: args.source.cache.clone(),
},
writer: args.writer.clone(),
writer_options: writer_pairs,
output: args.output.clone(),
verbose: args.verbose,
};
let writer = render_args.writer();
render_spec(spec, &render_args, &writer);
}
}
#[cfg(not(feature = "window"))]
Expand Down
36 changes: 28 additions & 8 deletions src/writer/hephaestus/canvas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,34 @@ pub const CANVAS_SIZE_OPTIONS: &[&str] = &["width", "height", "units"];
/// Units a `width` / `height` option may be given in.
const UNITS: &[&str] = &["px", "in", "cm", "mm", "pt"];

/// Size, resolution and background for one rendered figure.
/// Pixel dimensions and resolution for one rendered figure.
///
/// The one place width/height/dpi are collected: a [`Canvas`] carries them for
/// a file render, and the plot viewer reports them as the frame a plot was last
/// drawn in, so a file render can match what was on screen.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Canvas {
pub struct Dimensions {
pub width: u32,
pub height: u32,
pub dpi: f64,
}

impl Dimensions {
/// Pixel dimensions at the given resolution.
pub fn new(width: u32, height: u32, dpi: f64) -> Self {
Self { width, height, dpi }
}

/// The dimensions as a hephaestus size, for `PlotComposition::render`.
pub fn size(&self) -> Size {
Size::new(self.width as f64, self.height as f64)
}
}

/// Size, resolution and background for one rendered figure.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Canvas {
pub dims: Dimensions,
pub background: Color,
/// Whether the dimensions were given in a physical unit rather than pixels.
///
Expand All @@ -62,9 +84,7 @@ impl Canvas {
/// A canvas of the given pixel dimensions and DPI, on white.
pub fn new(width: u32, height: u32, dpi: f64) -> Self {
Self {
width,
height,
dpi,
dims: Dimensions::new(width, height, dpi),
background: rgba(1.0, 1.0, 1.0, 1.0),
physical: false,
}
Expand Down Expand Up @@ -121,7 +141,7 @@ impl Canvas {

/// The canvas as a hephaestus size, for `PlotComposition::render`.
pub fn size(&self) -> Size {
Size::new(self.width as f64, self.height as f64)
self.dims.size()
}

/// The resolution to record in an output that can carry one.
Expand All @@ -130,7 +150,7 @@ impl Canvas {
/// an image rendered at a higher resolution would claim the wrong physical
/// size.
pub fn dpi_hint(&self) -> Option<f64> {
Some(self.dpi)
Some(self.dims.dpi)
}

/// The background as a vector backend wants it: `None` when fully
Expand Down Expand Up @@ -245,7 +265,7 @@ pub(super) fn assert_canvas_semantics<W: crate::writer::Writer + Canvased + std:
let dims = |pairs: &[&str]| -> (u32, u32, f64) {
let writer = build(pairs).unwrap();
let c = writer.canvas();
(c.width, c.height, c.dpi)
(c.dims.width, c.dims.height, c.dims.dpi)
};

// No options: the documented defaults, on an opaque white canvas.
Expand Down
9 changes: 5 additions & 4 deletions src/writer/hephaestus/hep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,11 @@ impl HepWriter {
// rather than transparent, so a transparent canvas has to travel as a
// colour with zero alpha or a consumer paints white behind the plot.
options.background = Some(self.canvas.background);
options.size_hint = self
.size_asked
.then_some((self.canvas.width as f64, self.canvas.height as f64));
options.dpi_hint = self.dpi_asked.then_some(self.canvas.dpi);
options.size_hint = self.size_asked.then_some((
self.canvas.dims.width as f64,
self.canvas.dims.height as f64,
));
options.dpi_hint = self.dpi_asked.then_some(self.canvas.dims.dpi);
options.embed_fonts = self.embed_fonts;
options.embed_images = false;
options
Expand Down
4 changes: 2 additions & 2 deletions src/writer/hephaestus/jpeg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ impl JpegWriter {
) -> Result<Vec<u8>> {
let pixels = raster::pixels(spec, data, &self.canvas, renderer)?;
encode_jpeg(
self.canvas.width,
self.canvas.height,
self.canvas.dims.width,
self.canvas.dims.height,
&pixels,
self.quality,
self.canvas.background,
Expand Down
4 changes: 3 additions & 1 deletion src/writer/hephaestus/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ mod window;
pub use hephaestus::color::{rgba, Color};

pub use canvas::Canvas;
#[cfg(feature = "window")]
pub use canvas::Dimensions;
#[cfg(feature = "hep")]
use canvas::CANVAS_SIZE_OPTIONS;

Expand All @@ -84,7 +86,7 @@ pub use tiff::{TiffCompression, TiffWriter};
#[cfg(feature = "webp")]
pub use webp::WebpWriter;
#[cfg(feature = "window")]
pub use window::PlotViewer;
pub use window::{PlotViewer, VIEWER_OPTIONS};

// Re-exported so a caller can name the setting without depending on the
// renderer crate; the variants are the format's own vocabulary.
Expand Down
3 changes: 2 additions & 1 deletion src/writer/hephaestus/pdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ impl PdfWriter {
spec: &Plot,
data: &HashMap<String, DataFrame>,
) -> Result<(Vec<u8>, Vec<String>)> {
let mut scene = PdfScene::with_config(self.canvas.size(), self.canvas.dpi, self.config());
let mut scene =
PdfScene::with_config(self.canvas.size(), self.canvas.dims.dpi, self.config());
vector::draw(spec, data, &self.canvas, &mut scene)?;
Ok((encode_pdf(&scene), describe(scene.warnings())))
}
Expand Down
4 changes: 2 additions & 2 deletions src/writer/hephaestus/png.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ impl PngWriter {
// `render_to_buffer` hands out straight (un-premultiplied) alpha, which
// is exactly what PNG stores, so the buffer encodes as-is.
encode_png(
self.canvas.width,
self.canvas.height,
self.canvas.dims.width,
self.canvas.dims.height,
&pixels,
self.compression,
self.canvas.dpi_hint(),
Expand Down
15 changes: 10 additions & 5 deletions src/writer/hephaestus/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ pub const MAX_RASTER_DIMENSION: u32 = MAX_TEXTURE_DIMENSION;
///
/// Returns `GgsqlError::WriterError` naming the limit and the alternatives.
fn check_size(canvas: &Canvas) -> Result<()> {
if canvas.width > MAX_RASTER_DIMENSION || canvas.height > MAX_RASTER_DIMENSION {
if canvas.dims.width > MAX_RASTER_DIMENSION || canvas.dims.height > MAX_RASTER_DIMENSION {
return Err(GgsqlError::WriterError(format!(
"{}x{} is too large to rasterise: no dimension may exceed {} px, and a \
given GPU may allow less. The svg and pdf writers have no such limit \
and are resolution independent, so they are the better choice at this size",
canvas.width, canvas.height, MAX_RASTER_DIMENSION
canvas.dims.width, canvas.dims.height, MAX_RASTER_DIMENSION
)));
}
Ok(())
Expand All @@ -89,12 +89,17 @@ pub fn render_rgba8(
{
let scene = renderer.0.scene();
scene.clear();
view.render(scene, canvas.size(), canvas.dpi);
view.render(scene, canvas.size(), canvas.dims.dpi);
}
let mut pixels = vec![0u8; (canvas.width as usize) * (canvas.height as usize) * 4];
let mut pixels = vec![0u8; (canvas.dims.width as usize) * (canvas.dims.height as usize) * 4];
renderer
.0
.render_to_buffer(canvas.width, canvas.height, canvas.background, &mut pixels)
.render_to_buffer(
canvas.dims.width,
canvas.dims.height,
canvas.background,
&mut pixels,
)
.map_err(|e| GgsqlError::WriterError(format!("render failed: {e}")))?;
Ok(pixels)
}
Expand Down
3 changes: 2 additions & 1 deletion src/writer/hephaestus/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ impl SvgWriter {
spec: &Plot,
data: &HashMap<String, DataFrame>,
) -> Result<(String, Vec<String>)> {
let mut scene = SvgScene::with_config(self.canvas.size(), self.canvas.dpi, self.config());
let mut scene =
SvgScene::with_config(self.canvas.size(), self.canvas.dims.dpi, self.config());
vector::draw(spec, data, &self.canvas, &mut scene)?;
Ok((encode_svg(&scene), describe(scene.warnings())))
}
Expand Down
4 changes: 2 additions & 2 deletions src/writer/hephaestus/tiff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ impl TiffWriter {
) -> Result<Vec<u8>> {
let pixels = raster::pixels(spec, data, &self.canvas, renderer)?;
encode_tiff(
self.canvas.width,
self.canvas.height,
self.canvas.dims.width,
self.canvas.dims.height,
&pixels,
self.compression,
self.canvas.dpi_hint(),
Expand Down
2 changes: 1 addition & 1 deletion src/writer/hephaestus/vector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ pub fn draw(
scene: &mut dyn SceneBuilder,
) -> Result<()> {
let mut view = compose::prepare(spec, data)?;
view.render(scene, canvas.size(), canvas.dpi);
view.render(scene, canvas.size(), canvas.dims.dpi);
Ok(())
}
4 changes: 2 additions & 2 deletions src/writer/hephaestus/webp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ impl WebpWriter {
// Straight alpha in, straight alpha out — VP8L stores exactly the
// buffer the renderer read back.
encode_webp(
self.canvas.width,
self.canvas.height,
self.canvas.dims.width,
self.canvas.dims.height,
&pixels,
self.canvas.dpi_hint(),
)
Expand Down
Loading
Loading