Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
234 changes: 216 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ parley = "0.6"
skrifa = "0.40"
polycool = "0.4"
# Linebender ecosystem (END)
krilla = "0.6"
krilla-svg = "0.3"
usvg-045 = { package = "usvg", version = "0.45" }
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9"
glam = { version = "0.29", default-features = false, features = [
Expand Down
3 changes: 3 additions & 0 deletions editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ vello = { workspace = true }
base64 = { workspace = true }
spin = { workspace = true }
image = { workspace = true }
krilla = { workspace = true }
krilla-svg = { workspace = true }
usvg-045 = { workspace = true }

# Optional local dependencies
wgpu-executor = { workspace = true, optional = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ impl DialogLayoutHolder for ExportDialogMessageHandler {

impl LayoutHolder for ExportDialogMessageHandler {
fn layout(&self) -> Layout {
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG")]
let entries = [(FileType::Png, "PNG"), (FileType::Jpg, "JPG"), (FileType::Svg, "SVG"), (FileType::Pdf, "PDF")]
.into_iter()
.map(|(file_type, name)| {
RadioEntryData::new(format!("{file_type:?}"))
Expand All @@ -114,7 +114,7 @@ impl LayoutHolder for ExportDialogMessageHandler {
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.increment_step(0.5)
.disabled(self.file_type == FileType::Svg)
.disabled(self.file_type == FileType::Svg || self.file_type == FileType::Pdf)
.on_update(|number_input: &NumberInput| ExportDialogMessage::ScaleFactor { factor: number_input.value.unwrap() }.into())
.min_width(200)
.widget_instance(),
Expand Down
2 changes: 2 additions & 0 deletions editor/src/messages/frontend/utility_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub enum FileType {
Png,
Jpg,
Svg,
Pdf,
}

impl FileType {
Expand All @@ -55,6 +56,7 @@ impl FileType {
FileType::Png => "image/png",
FileType::Jpg => "image/jpeg",
FileType::Svg => "image/svg+xml",
FileType::Pdf => "application/pdf",
}
}
}
Expand Down
46 changes: 42 additions & 4 deletions editor/src/node_graph_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,10 @@ impl NodeGraphExecutor {
pub fn submit_document_export(&mut self, document: &mut DocumentMessageHandler, document_id: DocumentId, mut export_config: ExportConfig) -> Result<(), String> {
let network = document.network_interface.document_network().clone();

let export_format = if export_config.file_type == FileType::Svg {
graphene_std::application_io::ExportFormat::Svg
} else {
graphene_std::application_io::ExportFormat::Raster
let export_format = match export_config.file_type {
FileType::Svg => graphene_std::application_io::ExportFormat::Svg,
FileType::Pdf => graphene_std::application_io::ExportFormat::Pdf,
_ => graphene_std::application_io::ExportFormat::Raster,
};

// Calculate the bounding box of the region to be exported
Expand Down Expand Up @@ -440,6 +440,7 @@ impl NodeGraphExecutor {
FileType::Svg => "svg",
FileType::Png => "png",
FileType::Jpg => "jpg",
FileType::Pdf => "pdf",
};
let base_name = match (artboard_name, artboard_count) {
(Some(artboard_name), count) if count > 1 => format!("{name} - {artboard_name}"),
Expand All @@ -457,6 +458,11 @@ impl NodeGraphExecutor {
name,
content: svg.into_bytes().into(),
});
} else if file_type == FileType::Pdf {
match svg_to_pdf(&svg, size.into()) {
Ok(pdf_data) => responses.add(FrontendMessage::TriggerSaveFile { name, content: pdf_data.into() }),
Err(err) => return Err(format!("Failed to convert SVG to PDF: {err}")),
}
} else {
let mime = file_type.to_mime().to_string();
let size = size.as_dvec2().into();
Expand Down Expand Up @@ -500,6 +506,9 @@ impl NodeGraphExecutor {
FileType::Svg => {
return Err("SVG cannot be exported from an image buffer".to_string());
}
FileType::Pdf => {
return Err("PDF cannot be exported from an image buffer".to_string());
}
}

responses.add(FrontendMessage::TriggerSaveFile { name, content: encoded.into() });
Expand All @@ -513,6 +522,35 @@ impl NodeGraphExecutor {
}
}

/// Convert an SVG string to PDF bytes using krilla and krilla-svg.
fn svg_to_pdf(svg: &str, size: DVec2) -> Result<Vec<u8>, String> {
use krilla::page::PageSettings;
use krilla_svg::{SurfaceExt, SvgSettings};

// Parse the SVG with usvg 0.45 (matching krilla-svg's expected version)
let options = usvg_045::Options::default();
let tree = usvg_045::Tree::from_str(svg, &options).map_err(|e| format!("Failed to parse SVG for PDF conversion: {e}"))?;

// Create a krilla PDF document
let mut document = krilla::Document::new();

let page_width = size.x.max(1.) as f32;
let page_height = size.y.max(1.) as f32;
let page_settings = PageSettings::from_wh(page_width, page_height).ok_or_else(|| "Invalid page dimensions for PDF export".to_string())?;

let mut page = document.start_page_with(page_settings);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The check for valid dimensions is redundant. PageSettings::from_wh on line 536 already validates that page_width and page_height are valid. This second check is unnecessary and can be replaced with expect() to make the code cleaner and slightly more performant.

Suggested change
let mut page = document.start_page_with(page_settings);
let krilla_size = krilla::geom::Size::from_wh(page_width, page_height).expect("Dimensions already validated by PageSettings");

let mut surface = page.surface();

let krilla_size = krilla::geom::Size::from_wh(page_width, page_height).ok_or_else(|| "Invalid size for PDF export".to_string())?;
surface.draw_svg(&tree, krilla_size, SvgSettings::default());

surface.finish();
page.finish();

let pdf_bytes = document.finish().map_err(|e| format!("Failed to generate PDF: {e:?}"))?;
Ok(pdf_bytes)
}

// Re-export for usage by tests in other modules
#[cfg(test)]
pub use test::Instrumented;
Expand Down
1 change: 1 addition & 0 deletions node-graph/libraries/application-io/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ pub enum ExportFormat {
#[default]
Svg,
Raster,
Pdf,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
Expand Down
2 changes: 1 addition & 1 deletion node-graph/nodes/gstd/src/render_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async fn create_context<'a: 'n>(
let footprint = render_config.viewport;

let render_output_type = match render_config.export_format {
ExportFormat::Svg => RenderOutputTypeRequest::Svg,
ExportFormat::Svg | ExportFormat::Pdf => RenderOutputTypeRequest::Svg,
ExportFormat::Raster => RenderOutputTypeRequest::Vello,
};

Expand Down
Loading