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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/app/src/ui/canvas/image_painting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub(super) fn paint_document(app: &PlotxApp, ci: usize, rect: egui::Rect, painte
items[object_index] = plotx_render::DocumentItem::Raster(plotx_render::DocumentRaster {
source_hash: raster_cache_hash(asset.sha256, image.page_index),
frame,
pixels: preview.rgba8.as_slice(),
pixels: preview.rgba8.clone(),
pixel_size: preview.pixel_size,
source_pixel_size: if image.page_index == 0 {
asset.pixel_size
Expand Down
6 changes: 1 addition & 5 deletions crates/app/src/ui/canvas/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,7 @@ fn resize_cursor(handle: ResizeHandle) -> egui::CursorIcon {
mod tests {
use super::*;

use plotx_core::state::{
CanvasObject, CanvasObjectKind, CanvasViewport, PanelMeta, PlotObject, TextBox,
};
use plotx_core::state::{CanvasObject, CanvasObjectKind, CanvasViewport, PlotObject, TextBox};
use plotx_figure::{Axis, Figure};

#[test]
Expand Down Expand Up @@ -648,7 +646,6 @@ mod tests {
plotx_core::state::AxisOverrides::default(),
figure,
viewport,
PanelMeta::new("title".to_owned(), 50.0),
)
})),
});
Expand Down Expand Up @@ -682,7 +679,6 @@ mod tests {
plotx_core::state::AxisOverrides::default(),
figure,
viewport,
PanelMeta::new("title".to_owned(), 50.0),
)
})),
});
Expand Down
3 changes: 1 addition & 2 deletions crates/app/src/ui/canvas/navigation_tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::*;
use plotx_core::state::{CanvasViewport, PanelMeta, PlotObject};
use plotx_core::state::{CanvasViewport, PlotObject};
use plotx_figure::{Axis, Figure};

const PLOT_ID: ObjectId = ObjectId::new(1);
Expand Down Expand Up @@ -32,7 +32,6 @@ fn zoomed_plot_fixture() -> (PlotxApp, ObjectId, PlotRect) {
plotx_core::state::AxisOverrides::default(),
figure,
viewport,
PanelMeta::new("title".to_owned(), 50.0),
))),
});
app.doc.canvases.push(canvas);
Expand Down
18 changes: 13 additions & 5 deletions crates/app/src/ui/clipboard_figure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
use std::fmt;

use egui::Context;
use plotx_core::export::{RasterError, RasterImage, RasterOptions, rasterize_canvas};
use plotx_core::export::{
ExportError, MissingImagePolicy, RasterError, RasterImage, RasterOptions,
prepare_render_document, rasterize_svg,
};
use plotx_core::operation::{Diagnostic, DiagnosticCode, OperationKind, OperationReport, Severity};
use plotx_core::state::{BoardFrameId, PlotxApp};

Expand Down Expand Up @@ -92,15 +95,16 @@ fn build_payload(
.get(canvas_index)
.ok_or(ClipboardFigureError::NoTarget)?;
let dpi = app.settings.export.dpi;
let raster =
rasterize_canvas(canvas, RasterOptions::new(dpi)).map_err(ClipboardFigureError::Raster)?;
let document = prepare_render_document(canvas, &app.doc.assets, MissingImagePolicy::Block)
.map_err(ClipboardFigureError::Export)?;
let svg = plotx_render::svg::export_document(&document);
let raster = rasterize_svg(&svg, canvas.size_pt(), RasterOptions::new(dpi))
.map_err(ClipboardFigureError::Raster)?;
#[cfg(windows)]
{
let png = raster
.to_png()
.map_err(|error| ClipboardFigureError::PngEncode(error.into()))?;
let document = plotx_core::state::build_render_document(canvas);
let svg = plotx_render::svg::export_document(&document);
let emf =
plotx_render::emf::export_document_emf(&document).map_err(|error| error.to_string());
Ok(FigurePayload {
Expand Down Expand Up @@ -293,6 +297,7 @@ fn clipboard_error_code(error: &ClipboardFigureError) -> DiagnosticCode {
fn clipboard_error_category(error: &ClipboardFigureError) -> &'static str {
match error {
ClipboardFigureError::NoTarget => "no_target",
ClipboardFigureError::Export(_) => "embedded_image",
ClipboardFigureError::Raster(_) => "rasterization",
#[cfg(windows)]
ClipboardFigureError::PngEncode(_) => "png_encode",
Expand All @@ -306,6 +311,7 @@ fn clipboard_error_category(error: &ClipboardFigureError) -> &'static str {
#[derive(Debug)]
enum ClipboardFigureError {
NoTarget,
Export(ExportError),
Raster(RasterError),
#[cfg(windows)]
PngEncode(Box<dyn std::error::Error + Send + Sync>),
Expand All @@ -322,6 +328,7 @@ impl fmt::Display for ClipboardFigureError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoTarget => formatter.write_str("no canvas is selected or active"),
Self::Export(error) => error.fmt(formatter),
Self::Raster(error) => error.fmt(formatter),
#[cfg(windows)]
Self::PngEncode(error) => write!(formatter, "PNG encoding failed: {error}"),
Expand All @@ -342,6 +349,7 @@ impl std::error::Error for ClipboardFigureError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Raster(error) => Some(error),
Self::Export(error) => Some(error),
_ => None,
}
}
Expand Down
11 changes: 2 additions & 9 deletions crates/app/src/ui/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -419,15 +419,8 @@ pub fn describe(app: &PlotxApp, id: CommandId) -> CommandDescriptor {
),
CommandId::Export(_) => requires(has_canvas, "Open a canvas before exporting a figure."),
CommandId::CopyFigure => requires(
super::clipboard_figure::resolve_copy_target(app).is_some_and(|canvas| {
!app.doc.canvases[canvas].objects.iter().any(|item| {
matches!(
item.kind,
plotx_core::state::CanvasObjectKind::RasterImage(_)
)
})
}),
"Open a figure without external images; Copy Figure cannot include them yet.",
super::clipboard_figure::resolve_copy_target(app).is_some(),
"Open a canvas before copying a figure.",
),
CommandId::Undo => requires(app.can_undo(), "Nothing to undo yet."),
CommandId::Redo => requires(app.can_redo(), "Nothing to redo yet."),
Expand Down
122 changes: 109 additions & 13 deletions crates/app/src/ui/export_dialog.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::*;
use plotx_core::export::{
ComplianceStatus, ExportPreset, PrecheckReport, page_metrics, precheck_report,
ComplianceStatus, ExportPreset, PrecheckReport, image_precheck_items, page_metrics,
precheck_report,
};
use plotx_core::settings::{MAX_EXPORT_DPI, MIN_EXPORT_DPI};

Expand Down Expand Up @@ -99,11 +100,30 @@ pub(super) fn export_options_window(app: &mut PlotxApp, ctx: &egui::Context) {
Empty pages keep their original size.",
);

let has_images = selected_pages_have_images(
&app.doc.canvases,
pending.scope,
active_page,
page_count,
);
if has_images {
ui.add_space(8.0);
ui.checkbox(
&mut pending.allow_missing_images,
"Export with missing-image placeholders",
)
.on_hover_text(
"If an embedded image cannot be read, export a labelled placeholder instead of stopping.",
);
} else {
pending.allow_missing_images = false;
}

let preset = pending.preset;
let scope = pending.scope;
let dpi = pending.dpi;
if let Some(preset) = preset {
let report = build_report(app, preset, scope, dpi, active_page, page_count);
let report = build_report(app, preset, scope, dpi, active_page, page_count);
if !report.items.is_empty() {
ui.add_space(10.0);
ui.separator();
draw_precheck(ui, &report);
Expand Down Expand Up @@ -138,6 +158,25 @@ pub(super) fn export_options_window(app: &mut PlotxApp, ctx: &egui::Context) {
}
}

fn selected_pages_have_images(
canvases: &[plotx_core::state::CanvasDocument],
scope: ExportPageScope,
active_page: usize,
page_count: usize,
) -> bool {
plotx_core::export::resolve_page_scope(scope, Some(active_page), page_count)
.unwrap_or_default()
.into_iter()
.filter_map(|page| canvases.get(page))
.flat_map(|canvas| &canvas.objects)
.any(|item| {
matches!(
&item.kind,
plotx_core::state::CanvasObjectKind::RasterImage(_)
)
})
}

fn set_confirmed_trim_default(app: &mut PlotxApp, trim_to_visible_content: bool) {
let target = app.app_target();
match app.plan_property_write(
Expand All @@ -156,26 +195,40 @@ fn set_confirmed_trim_default(app: &mut PlotxApp, trim_to_visible_content: bool)

fn build_report(
app: &PlotxApp,
preset: ExportPreset,
preset: Option<ExportPreset>,
scope: ExportPageScope,
dpi: u16,
active_page: usize,
page_count: usize,
) -> PrecheckReport {
let pages = plotx_core::export::resolve_page_scope(scope, Some(active_page), page_count)
.unwrap_or_else(|_| vec![active_page]);
let metrics: Vec<_> = pages
let mut report = if let Some(preset) = preset {
let metrics: Vec<_> = pages
.iter()
.filter_map(|&page| app.doc.canvases.get(page))
.map(page_metrics)
.collect();
precheck_report(
&metrics,
preset.target_width_mm(),
&preset.thresholds(),
preset.format(),
dpi,
)
} else {
PrecheckReport { items: Vec::new() }
};
let canvases: Vec<_> = pages
.iter()
.filter_map(|&page| app.doc.canvases.get(page))
.map(page_metrics)
.collect();
precheck_report(
&metrics,
preset.target_width_mm(),
&preset.thresholds(),
preset.format(),
dpi,
)
report.items.extend(image_precheck_items(
&canvases,
&app.doc.assets,
preset.and_then(ExportPreset::target_width_mm),
));
report
}

fn draw_precheck(ui: &mut Ui, report: &PrecheckReport) {
Expand Down Expand Up @@ -206,6 +259,49 @@ fn status_dot(ui: &mut Ui, status: ComplianceStatus) {
ui.painter().circle_filled(rect.center(), 4.0, color);
}

#[cfg(test)]
mod image_tests {
use super::*;
use plotx_core::state::{
AssetId, CanvasObject, CanvasObjectKind, ObjectFrame, ObjectId, RasterImageContent,
};

#[test]
fn placeholder_option_is_available_for_every_scope_containing_an_image() {
let plain = plotx_core::state::CanvasDocument::new("plain".to_owned(), [100.0, 80.0]);
let mut image_page =
plotx_core::state::CanvasDocument::new("image".to_owned(), [100.0, 80.0]);
image_page.objects.push(CanvasObject {
id: ObjectId::new(1),
name: "image".to_owned(),
frame: ObjectFrame::new(0.0, 0.0, 20.0, 20.0),
locked: false,
visible: true,
kind: CanvasObjectKind::RasterImage(RasterImageContent::new(AssetId::new())),
});
let pages = [plain, image_page];

assert!(!selected_pages_have_images(
&pages,
ExportPageScope::Current,
0,
pages.len(),
));
assert!(selected_pages_have_images(
&pages,
ExportPageScope::Current,
1,
pages.len(),
));
assert!(selected_pages_have_images(
&pages,
ExportPageScope::All,
0,
pages.len(),
));
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/app/src/ui/file_dialogs/image_import/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub(super) fn insert_proxy(
hash,
page_index,
pixel_size: proxy.pixel_size,
rgba8: Arc::new(proxy.rgba8),
rgba8: Arc::from(proxy.rgba8),
});
while app
.session
Expand Down
4 changes: 3 additions & 1 deletion crates/core/src/automation/tool_executors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,15 +455,17 @@ pub(super) fn execute_export(
});
continue;
}
let written = match crate::export::export_canvases(
let written = match crate::export::export_canvases_with_assets(
std::slice::from_ref(canvas),
&app.doc.assets,
Some(0),
&ExportSettings {
format,
scope: ExportPageScope::Current,
dpi: params.dpi,
target_width_mm: None,
trim_to_visible_content: false,
allow_missing_images: false,
},
&base,
) {
Expand Down
Loading
Loading