Skip to content
Open
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
278 changes: 259 additions & 19 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 @@ -155,6 +155,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
1 change: 1 addition & 0 deletions editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true } # NOTE: `core-types` should not be added here because `graphene-std` re-exports its contents
rendering = { workspace = true }
preprocessor = { workspace = true }

# Workspace dependencies
Expand Down
4 changes: 4 additions & 0 deletions editor/src/messages/dialog/dialog_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ impl MessageHandler<DialogMessage, DialogMessageContext<'_>> for DialogMessageHa
self.export_dialog.bounds = ExportBounds::AllArtwork;
}

if self.export_dialog.bounds == ExportBounds::ArtboardsAsPages && self.export_dialog.artboards.len() < 2 {
self.export_dialog.bounds = ExportBounds::AllArtwork;
}

self.export_dialog.has_selection = document.network_interface.selected_nodes().selected_layers(document.metadata()).next().is_some();
self.export_dialog.send_dialog_to_frontend(responses);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,21 @@ impl MessageHandler<ExportDialogMessage, ExportDialogMessageContext<'_>> for Exp
ExportDialogMessage::ExportBounds { bounds } => self.bounds = bounds,

ExportDialogMessage::Submit => {
let artboards_as_pages = self.bounds == ExportBounds::ArtboardsAsPages;
let artboard_name = match self.bounds {
ExportBounds::Artboard(layer) => self.artboards.get(&layer).cloned(),
_ => None,
};
let bounds = if artboards_as_pages { ExportBounds::AllArtwork } else { self.bounds };
responses.add_front(PortfolioMessage::SubmitDocumentExport {
name: portfolio.active_document().map(|document| document.name.clone()).unwrap_or_default(),
file_type: self.file_type,
scale_factor: self.scale_factor,
bounds: self.bounds,
bounds,
transparent_background: self.file_type != FileType::Jpg && self.transparent_background,
artboard_name,
artboard_count: self.artboards.len(),
artboards_as_pages,
})
}
}
Expand Down Expand Up @@ -91,7 +94,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 @@ -101,40 +104,49 @@ impl LayoutHolder for ExportDialogMessageHandler {
.collect();

let export_type = vec![
TextLabel::new("File Type").table_align(true).min_width(100).widget_instance(),
TextLabel::new("File Type").table_align(true).min_width(120).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
RadioInput::new(entries).selected_index(Some(self.file_type as u32)).widget_instance(),
];

let resolution = vec![
TextLabel::new("Scale Factor").table_align(true).min_width(100).widget_instance(),
TextLabel::new("Scale Factor").table_align(true).min_width(120).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
NumberInput::new(Some(self.scale_factor))
.unit("")
.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(),
];

let standard_bounds = vec![
let standard_bounds: Vec<(ExportBounds, String, bool)> = vec![
(ExportBounds::AllArtwork, "All Artwork".to_string(), false),
(ExportBounds::Selection, "Selection".to_string(), !self.has_selection),
];
let artboards = self.artboards.iter().map(|(&layer, name)| (ExportBounds::Artboard(layer), name.to_string(), false)).collect();
let choices = [standard_bounds, artboards];

let artboards_as_pages: Vec<(ExportBounds, String, bool)> = if self.file_type == FileType::Pdf && self.artboards.len() >= 2 {
vec![(ExportBounds::ArtboardsAsPages, "Artboards as Pages".to_string(), false)]
} else {
vec![]
};

let artboards: Vec<(ExportBounds, String, bool)> = self.artboards.iter().map(|(&layer, name)| (ExportBounds::Artboard(layer), name.to_string(), false)).collect();
let choices = [standard_bounds, artboards_as_pages, artboards];

let current_bounds = if !self.has_selection && self.bounds == ExportBounds::Selection {
ExportBounds::AllArtwork
} else if self.bounds == ExportBounds::ArtboardsAsPages && (self.file_type != FileType::Pdf || self.artboards.len() < 2) {
ExportBounds::AllArtwork
} else {
self.bounds
};
let index = choices.iter().flatten().position(|(bounds, _, _)| *bounds == current_bounds).unwrap_or(0);

let mut entries = choices
let mut entries: Vec<Vec<_>> = choices
.into_iter()
.map(|choice| {
choice
Expand All @@ -149,19 +161,17 @@ impl LayoutHolder for ExportDialogMessageHandler {
})
.collect::<Vec<_>>();

if entries[1].is_empty() {
entries.remove(1);
}
entries.retain(|section| !section.is_empty());

let export_area = vec![
TextLabel::new("Bounds").table_align(true).min_width(100).widget_instance(),
TextLabel::new("Bounds").table_align(true).min_width(120).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
DropdownInput::new(entries).selected_index(Some(index as u32)).widget_instance(),
];

let checkbox_id = CheckboxId::new();
let transparent_background = vec![
TextLabel::new("Transparency").table_align(true).min_width(100).for_checkbox(checkbox_id).widget_instance(),
TextLabel::new("Transparency").table_align(true).min_width(120).for_checkbox(checkbox_id).widget_instance(),
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
CheckboxInput::new(self.transparent_background)
.disabled(self.file_type == FileType::Jpg)
Expand All @@ -170,11 +180,13 @@ impl LayoutHolder for ExportDialogMessageHandler {
.widget_instance(),
];

Layout(vec![
let rows = vec![
LayoutGroup::row(export_type),
LayoutGroup::row(resolution),
LayoutGroup::row(export_area),
LayoutGroup::row(transparent_background),
])
];

Layout(rows)
}
}
3 changes: 3 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 All @@ -66,6 +68,7 @@ pub enum ExportBounds {
AllArtwork,
Selection,
Artboard(LayerNodeIdentifier),
ArtboardsAsPages,
}

#[cfg_attr(feature = "wasm", derive(tsify::Tsify), tsify(large_number_types_as_bigints))]
Expand Down
1 change: 1 addition & 0 deletions editor/src/messages/portfolio/portfolio_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ pub enum PortfolioMessage {
transparent_background: bool,
artboard_name: Option<String>,
artboard_count: usize,
artboards_as_pages: bool,
},
SubmitActiveGraphRender,
SubmitGraphRender {
Expand Down
85 changes: 69 additions & 16 deletions editor/src/messages/portfolio/portfolio_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION}
use crate::messages::animation::TimingInformation;
use crate::messages::clipboard::utility_types::ClipboardContent;
use crate::messages::dialog::simple_dialogs;
use crate::messages::frontend::utility_types::{DocumentDetails, OpenDocument};
use crate::messages::frontend::utility_types::{DocumentDetails, ExportBounds, FileType, OpenDocument};
use crate::messages::input_mapper::utility_types::input_keyboard::Key;
use crate::messages::input_mapper::utility_types::macros::{action_shortcut, action_shortcut_manual};
use crate::messages::layout::utility_types::widget_prelude::*;
Expand Down Expand Up @@ -1127,25 +1127,78 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
transparent_background,
artboard_name,
artboard_count,
artboards_as_pages,
} => {
let document = self.active_document_id.and_then(|id| self.documents.get_mut(&id)).expect("Tried to render non-existent document");
let export_config = ExportConfig {
name,
file_type,
scale_factor,
bounds,
transparent_background,
artboard_name,
artboard_count,
..Default::default()
let document_id = self.active_document_id.unwrap();

// When exporting All Artwork as a multi-page PDF with 2+ artboards,
// submit one export per artboard (in layer-stack order, top = page 1).
let artboard_layers: Vec<(LayerNodeIdentifier, String)> = if artboards_as_pages && bounds == ExportBounds::AllArtwork && file_type == FileType::Pdf {
document
.metadata()
.all_layers()
.filter(|&layer| document.network_interface.is_artboard(&layer.to_node(), &[]))
.map(|layer| {
let ab_name = document
.network_interface
.node_metadata(&layer.to_node(), &[])
.map(|m| m.persistent_metadata.display_name.clone())
.and_then(|n| if n.is_empty() { None } else { Some(n) })
.unwrap_or_else(|| "Artboard".to_string());
(layer, ab_name)
})
.collect()
} else {
Vec::new()
};
let result = self.executor.submit_document_export(document, self.active_document_id.unwrap(), export_config);

if let Err(description) = result {
responses.add(DialogMessage::DisplayDialogError {
title: "Unable to export document".to_string(),
description,
});
if artboard_layers.len() >= 2 {
// Multi-page PDF: one graph execution per artboard
let total_pages = artboard_layers.len();
let mut first_error: Option<String> = None;
for (layer, ab_name) in artboard_layers {
let per_artboard_config = ExportConfig {
name: name.clone(),
file_type,
scale_factor,
bounds: ExportBounds::Artboard(layer),
transparent_background,
artboard_name: Some(ab_name),
artboard_count: total_pages,
is_multipage_pdf: true,
pdf_pages_total: total_pages,
..Default::default()
};
if let Err(e) = self.executor.submit_document_export(document, document_id, per_artboard_config) {
first_error = Some(e);
break;
}
}
if let Some(description) = first_error {
responses.add(DialogMessage::DisplayDialogError {
title: "Unable to export document".to_string(),
description,
});
}
} else {
// Single-page / non-PDF path (unchanged)
let export_config = ExportConfig {
name,
file_type,
scale_factor,
bounds,
transparent_background,
artboard_name,
artboard_count,
..Default::default()
};
if let Err(description) = self.executor.submit_document_export(document, document_id, export_config) {
responses.add(DialogMessage::DisplayDialogError {
title: "Unable to export document".to_string(),
description,
});
}
}
}
PortfolioMessage::SubmitActiveGraphRender => {
Expand Down
Loading
Loading