mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-02 22:20:50 -06:00
M4.31: Save PDF dialog and document.exportPdf with PDF settings
PdfSettings (serde, camelCase) is the Save PDF model: Standard, Compatibility
PDF 1.4-2.0, General (Preserve editing, Thumbnails, Fast web view, View after
saving, Create layers), Compression, Marks and Bleeds, Output, Advanced and
Security. PdfOptions = settings + pages, title and date.
document.exportPdf {path?, preset?, artboard?|artboards?|range?, standard?,
compatibility?, ...sections} -> {path|dataBase64, bytes, warnings} applies the
options over the preset (null keeps the preset's value); PDF/X, passwords,
PDF/A-2b at 2.0 and out-of-range values are refused; options accepted but not
applied yet come back as warnings. document.export {format: pdf} and
document.formats take and list the same options.
The Save PDF dialog (file.export.pdf, ui.savePdfDialog) has Preset,
Standard and Compatibility on top and a section list; it runs the export
before asking for a path, so bad options never open a save dialog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b0a8d78926
commit
4304a7c0b1
@@ -8,7 +8,6 @@ use vectorcraft_doc::Document;
|
||||
|
||||
use super::super::*;
|
||||
use super::Format;
|
||||
use crate::EngineError;
|
||||
|
||||
const C: &str = "document.export";
|
||||
|
||||
@@ -113,11 +112,7 @@ pub fn encode(doc: &Document, format: &str, p: &Value) -> Result<Vec<u8>> {
|
||||
let opts = vectorcraft_svg::ExportOptions { artboard, outline_text: o.outline_text.unwrap_or(false), ..Default::default() };
|
||||
vectorcraft_svg::export(doc, &opts).into_bytes()
|
||||
}
|
||||
"pdf" => {
|
||||
let o: ArtboardPick = options(f, p)?;
|
||||
let opts = vectorcraft_pdf::PdfOptions { artboards: boards(o.resolve(n))?, ..Default::default() };
|
||||
super::super::rasterfx::export_pdf(doc, &opts).map_err(|e| EngineError::Other(e.to_string()))?
|
||||
}
|
||||
"pdf" => super::pdf::encode(C, doc, p)?.0,
|
||||
"png" | "jpg" | "webp" => {
|
||||
let o: RasterOptions = options(f, p)?;
|
||||
let region = doc.artboards[boards(o.boards.one(n))?].rect;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//! - `encode`: one encoder per writable format, with typed options parsed from the params.
|
||||
//! - `export`: `document.export` / `serialize` / `exportSelection` / `exportForScreens`.
|
||||
//! - `save`: `document.save`, `file.saveAsTemplate`.
|
||||
//! - `pdf`: PDF settings and presets for every PDF export, `document.exportPdf`.
|
||||
//!
|
||||
//! [`FORMATS`] is the single list of formats (append-only); open dialogs use [`open_filters`],
|
||||
//! agents query `document.formats`.
|
||||
@@ -13,6 +14,7 @@ mod batch;
|
||||
mod encode;
|
||||
mod export;
|
||||
mod load;
|
||||
pub mod pdf;
|
||||
mod save;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -57,7 +59,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Document",
|
||||
[],
|
||||
None,
|
||||
"{path?, format?: svg|pdf|png|jpg|webp|vectorcraft (default: from the path's extension, else png), artboard?: 0, artboards?: [i…], range?: \"1-3, 5\" (1-based; PDF writes one page per artboard, default all; the other formats write one artboard), scale?: 1 (raster), quality?: 90 (jpg), outlineText?: bool (svg)} → {path, format, bytes}; no path → {dataBase64, format, bytes}. Never changes the document's path",
|
||||
"{path?, format?: svg|pdf|png|jpg|webp|vectorcraft (default: from the path's extension, else png), artboard?: 0, artboards?: [i…], range?: \"1-3, 5\" (1-based; PDF writes one page per artboard, default all; the other formats write one artboard), scale?: 1 (raster), quality?: 90 (jpg), outlineText?: bool (svg), …the PDF options of document.exportPdf} → {path, format, bytes}; no path → {dataBase64, format, bytes}. Never changes the document's path",
|
||||
has_doc,
|
||||
export::export
|
||||
),
|
||||
@@ -105,7 +107,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FormatOption {
|
||||
pub name: &'static str,
|
||||
/// JSON type: `number`, `integer`, `boolean`, `string` or `array`.
|
||||
/// JSON type: `number`, `integer`, `boolean`, `string`, `array` or `object`.
|
||||
pub ty: &'static str,
|
||||
/// The default as a JSON literal (`"1"`, `"false"`, `"null"`).
|
||||
pub default: &'static str,
|
||||
@@ -200,16 +202,7 @@ pub const FORMATS: &[Format] = &[
|
||||
options: &[ARTBOARD, OUTLINE_TEXT],
|
||||
},
|
||||
reader("svgz", "SVG Compressed", &["svgz"], "image/svg+xml", false),
|
||||
Format {
|
||||
id: "pdf",
|
||||
label: "PDF",
|
||||
extensions: &["pdf"],
|
||||
mime: "application/pdf",
|
||||
read: true,
|
||||
write: true,
|
||||
raster: false,
|
||||
options: &[ARTBOARD, ARTBOARDS, RANGE],
|
||||
},
|
||||
Format { id: "pdf", label: "PDF", extensions: &["pdf"], mime: "application/pdf", read: true, write: true, raster: false, options: pdf::OPTIONS },
|
||||
reader("ai", "PDF-compatible .ai", &["ai"], "application/pdf", false),
|
||||
reader("ait", "PDF-compatible .ait template", &["ait"], "application/pdf", false),
|
||||
Format { id: "png", label: "PNG", extensions: &["png"], mime: "image/png", read: true, write: true, raster: true, options: &[ARTBOARD, SCALE] },
|
||||
@@ -346,3 +339,5 @@ fn write_or_return(path: Option<&str>, bytes: &[u8], extra: Value) -> Result<Val
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_pdf;
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//! PDF export settings for every PDF path (`document.export {format: pdf}`, Export for Screens,
|
||||
//! `document.exportPdf` and the Save PDF dialog): presets, the options parsed over them and the
|
||||
//! warnings that come back.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_pdf::{Overprint, PdfError, PdfOptions, PdfSettings};
|
||||
|
||||
use super::super::*;
|
||||
use super::{ArtboardPick, FormatOption, write_or_return};
|
||||
use crate::EngineError;
|
||||
|
||||
const C: &str = "document.exportPdf";
|
||||
|
||||
/// The built-in preset every PDF export starts from.
|
||||
pub const DEFAULT_PRESET: &str = "VectorCraft Default";
|
||||
|
||||
/// The PDF options `document.formats` lists; `document.exportPdf` documents every field.
|
||||
pub(super) const OPTIONS: &[FormatOption] = &[
|
||||
super::ARTBOARD,
|
||||
super::ARTBOARDS,
|
||||
super::RANGE,
|
||||
FormatOption { name: "preset", ty: "string", default: "\"VectorCraft Default\"", description: "the PDF preset the other options apply over" },
|
||||
FormatOption { name: "standard", ty: "string", default: "\"none\"", description: "none | pdfA2b (PDF/X is not supported yet)" },
|
||||
FormatOption { name: "compatibility", ty: "string", default: "\"1.7\"", description: "PDF version: 1.4 | 1.5 | 1.6 | 1.7 | 2.0" },
|
||||
FormatOption { name: "compression", ty: "object", default: "null", description: "{color, gray, mono, compressText} (see document.exportPdf)" },
|
||||
FormatOption { name: "marks", ty: "object", default: "null", description: "printer's marks (see document.exportPdf)" },
|
||||
FormatOption { name: "bleed", ty: "object", default: "null", description: "{useDocument, top, bottom, left, right} in points" },
|
||||
FormatOption { name: "output", ty: "object", default: "null", description: "colour conversion and output intent (see document.exportPdf)" },
|
||||
FormatOption { name: "advanced", ty: "object", default: "null", description: "{fontSubsetPercent, outlineText, overprint}" },
|
||||
FormatOption { name: "security", ty: "object", default: "null", description: "passwords (not supported yet) and permissions" },
|
||||
];
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![cmd!(
|
||||
"document.exportPdf",
|
||||
"Export PDF",
|
||||
[],
|
||||
None,
|
||||
"{path?, preset?: \"VectorCraft Default\", artboard? | artboards?: [i…] | range?: \"1-3, 5\" (1-based; default all, one page each), standard?: none|pdfA2b|pdfX1a|pdfX3|pdfX4, compatibility?: 1.4|1.5|1.6|1.7 (default)|2.0, preserveEditing?, thumbnails?, fastWebView?, viewAfterSaving?, createLayers?, compression?: {color?, gray?: {downsample: none|average|subsample|bicubic, ppi: 300, abovePpi: 450, compression: none|zip|jpeg|jpeg2000|auto, quality: minimum|low|medium|high|maximum}, mono?: {downsample, ppi: 1200, abovePpi: 1800, compression: none|ccittG3|ccittG4|zip|runLength}, compressText?: true}, marks?: {trim, registration, colorBars, pageInfo, kind: roman|japanese, weight: 0.25, offset: 6}, bleed?: {useDocument, top, bottom, left, right} (pt), output?: {conversion: none|destination|preserveNumbers, destination, profiles: none|all|destination|taggedSource, outputIntent, outputCondition, outputConditionId, registry, trapped}, advanced?: {fontSubsetPercent: 100, outlineText: true, overprint: preserve|discard}, security?: {openPassword, permissionsPassword, printing: none|low|high, changes: none|pages|forms|comments|any, copy, screenReader, plaintextMetadata}} → {path, bytes, warnings}; no path → {dataBase64, bytes, warnings}. The options apply over the preset (null keeps its value); options accepted but not applied yet come back as warnings; PDF/X, passwords and PDF/A-2b at 2.0 are refused. document.export {format: pdf} takes the same options",
|
||||
has_doc,
|
||||
export_pdf
|
||||
)]
|
||||
}
|
||||
|
||||
/// Every preset name (built-in first).
|
||||
pub fn presets() -> Vec<String> {
|
||||
vec![DEFAULT_PRESET.to_string()]
|
||||
}
|
||||
|
||||
fn preset(cmd: &str, name: &str) -> Result<PdfSettings> {
|
||||
if name.eq_ignore_ascii_case(DEFAULT_PRESET) || name.eq_ignore_ascii_case("default") {
|
||||
return Ok(PdfSettings::default());
|
||||
}
|
||||
Err(bad(cmd, format!("unknown PDF preset `{name}` (presets: {})", presets().join(", "))))
|
||||
}
|
||||
|
||||
/// Merge `over` into `base`: objects key by key (recursively), `null` keeps the base (the
|
||||
/// preset's value), anything else replaces.
|
||||
fn merge(base: &mut Value, over: &Value) {
|
||||
match (base, over) {
|
||||
(Value::Object(b), Value::Object(o)) => {
|
||||
for (k, v) in o.iter().filter(|(_, v)| !v.is_null()) {
|
||||
merge(b.entry(k.as_str()).or_insert(Value::Null), v);
|
||||
}
|
||||
}
|
||||
(b, o) => *b = o.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// A writer error as an engine error: bad settings are bad params of `cmd`.
|
||||
fn pdf_error(cmd: &str, e: PdfError) -> EngineError {
|
||||
match e {
|
||||
PdfError::BadSetting(_) | PdfError::Unsupported(_) => bad(cmd, e.to_string()),
|
||||
e => EngineError::Other(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The settings `p` asks for: its preset (default: [`DEFAULT_PRESET`]) with `p`'s options applied
|
||||
/// over it, checked. Keys that aren't PDF options (path, format…) are ignored.
|
||||
pub fn settings(cmd: &str, p: &Value) -> Result<PdfSettings> {
|
||||
let base = match str_param(p, "preset") {
|
||||
Some(name) => preset(cmd, name)?,
|
||||
None => PdfSettings::default(),
|
||||
};
|
||||
if !p.is_object() {
|
||||
return Ok(base);
|
||||
}
|
||||
let mut v = serde_json::to_value(&base).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
merge(&mut v, p);
|
||||
let s = PdfSettings::deserialize(&v).map_err(|e| bad(cmd, format!("PDF options: {e}")))?;
|
||||
s.check().map_err(|e| pdf_error(cmd, e))?;
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// The full export options for `doc`: settings plus the artboards `p` picks (default all).
|
||||
pub fn options(cmd: &str, doc: &Document, p: &Value) -> Result<PdfOptions> {
|
||||
let pick = if p.is_object() { ArtboardPick::deserialize(p).map_err(|e| bad(cmd, format!("PDF options: {e}")))? } else { ArtboardPick::default() };
|
||||
let artboards = pick.resolve(doc.artboards.len()).map_err(|e| bad(cmd, e))?;
|
||||
Ok(PdfOptions { settings: settings(cmd, p)?, artboards, ..Default::default() })
|
||||
}
|
||||
|
||||
/// Encode `doc` as PDF with the options in `p` → (bytes, warnings). Raster effects are rendered
|
||||
/// to images at the document's raster effects resolution.
|
||||
pub fn encode(cmd: &str, doc: &Document, p: &Value) -> Result<(Vec<u8>, Vec<String>)> {
|
||||
let opts = options(cmd, doc, p)?;
|
||||
let r = super::super::rasterfx::export_pdf_with_report(doc, &opts).map_err(|e| pdf_error(cmd, e))?;
|
||||
let mut warnings = r.warnings;
|
||||
if opts.settings.advanced.overprint == Overprint::Preserve && doc.layers.iter().any(|l| l.has_overprint()) {
|
||||
warnings.push("overprinting objects are written without overprint: overprint is not written to PDF yet".into());
|
||||
}
|
||||
Ok((r.bytes, warnings))
|
||||
}
|
||||
|
||||
fn export_pdf(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (bytes, warnings) = encode(C, &s.doc()?.doc, p)?;
|
||||
write_or_return(str_param(p, "path"), &bytes, json!({ "warnings": warnings }))
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! `document.exportPdf` and the PDF options of every PDF export.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::pdf::DEFAULT_PRESET;
|
||||
use super::*;
|
||||
|
||||
fn session(artboards: usize) -> Session {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": artboards})).unwrap();
|
||||
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 40, "height": 30})).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn b64(v: &Value) -> Vec<u8> {
|
||||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap()
|
||||
}
|
||||
|
||||
fn export(s: &mut Session, p: Value) -> Value {
|
||||
s.execute("document.exportPdf", &p).unwrap_or_else(|e| panic!("{p}: {e}"))
|
||||
}
|
||||
|
||||
fn warnings(v: &Value) -> Vec<String> {
|
||||
v["warnings"].as_array().expect("warnings").iter().map(|w| w.as_str().unwrap().to_string()).collect()
|
||||
}
|
||||
|
||||
fn pages(bytes: &[u8]) -> usize {
|
||||
vectorcraft_pdf::import(bytes).unwrap().artboards.len()
|
||||
}
|
||||
|
||||
/// `p` plus `format: pdf` (the same options through `document.export`).
|
||||
fn as_export(p: &Value) -> Value {
|
||||
let mut o = p.as_object().cloned().unwrap_or_default();
|
||||
o.insert("format".into(), json!("pdf"));
|
||||
Value::Object(o)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_pdf_writes_the_compatibility_header_and_the_range() {
|
||||
let mut s = session(3);
|
||||
let v = export(&mut s, json!({}));
|
||||
let pdf = b64(&v);
|
||||
assert!(pdf.starts_with(b"%PDF-1.7"));
|
||||
assert_eq!(pages(&pdf), 3);
|
||||
assert_eq!(v["bytes"], pdf.len());
|
||||
assert!(warnings(&v).is_empty(), "{v}");
|
||||
assert!(b64(&export(&mut s, json!({"compatibility": "1.5"}))).starts_with(b"%PDF-1.5"));
|
||||
assert!(b64(&export(&mut s, json!({"compatibility": "2.0"}))).starts_with(b"%PDF-2.0"));
|
||||
assert_eq!(pages(&b64(&export(&mut s, json!({"range": "1,3"})))), 2);
|
||||
assert_eq!(pages(&b64(&export(&mut s, json!({"artboards": [1]})))), 1);
|
||||
// The same options through document.export and document.serialize.
|
||||
let v = s.execute("document.export", &as_export(&json!({"compatibility": "1.4", "range": "2"}))).unwrap();
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.4") && pages(&b64(&v)) == 1);
|
||||
let v = s.execute("document.serialize", &as_export(&json!({"compatibility": "1.6"}))).unwrap();
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_options_are_refused() {
|
||||
let mut s = session(2);
|
||||
for p in [
|
||||
json!({"compatibility": "1.3"}),
|
||||
json!({"compatibility": 1.5}),
|
||||
json!({"range": "1-3"}),
|
||||
json!({"range": "0"}),
|
||||
json!({"artboards": []}),
|
||||
json!({"preset": "Nope"}),
|
||||
json!({"standard": "pdfX4"}),
|
||||
json!({"standard": "pdfA2b", "compatibility": "2.0"}),
|
||||
json!({"compression": {"color": {"ppi": 0}}}),
|
||||
json!({"compression": "zip"}),
|
||||
json!({"security": {"openPassword": "x"}}),
|
||||
] {
|
||||
let e = s.execute("document.exportPdf", &p).unwrap_err();
|
||||
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{p}: {e}");
|
||||
assert!(s.execute("document.export", &as_export(&p)).is_err(), "document.export {p}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_apply_over_the_preset() {
|
||||
let mut s = session(1);
|
||||
let a = b64(&export(&mut s, json!({"preset": DEFAULT_PRESET, "compression": {"compressText": false}})));
|
||||
let b = b64(&export(&mut s, json!({"preset": "default"})));
|
||||
assert!(a.len() > b.len(), "content streams left uncompressed");
|
||||
assert!(String::from_utf8_lossy(&a).contains(" re") || String::from_utf8_lossy(&a).contains(" l"));
|
||||
// `null` (the default document.formats lists) keeps the preset's value, at any depth.
|
||||
let p = json!({"compatibility": null, "compression": null, "security": {"openPassword": null}, "marks": {"weight": null}});
|
||||
let v = export(&mut s, p);
|
||||
assert!(b64(&v).starts_with(b"%PDF-1.7"));
|
||||
assert!(warnings(&v).is_empty(), "{v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdf_a_refuses_pdf_2() {
|
||||
let mut s = session(1);
|
||||
let e = s.execute("document.exportPdf", &json!({"standard": "pdfA2b", "compatibility": "2.0"})).unwrap_err();
|
||||
assert!(matches!(e, crate::EngineError::BadParams { .. }), "{e}");
|
||||
let pdf = b64(&export(&mut s, json!({"standard": "pdfA2b", "compatibility": "1.6"})));
|
||||
assert!(pdf.starts_with(b"%PDF-1.6") && String::from_utf8_lossy(&pdf).contains("pdfaid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn options_not_applied_yet_and_document_features_warn() {
|
||||
let mut s = session(1);
|
||||
let w = warnings(&export(&mut s, json!({"thumbnails": true, "marks": {"trim": true}})));
|
||||
assert_eq!(w.len(), 2, "{w:?}");
|
||||
assert!(w.iter().any(|w| w.contains("thumbnails")) && w.iter().any(|w| w.contains("marks")));
|
||||
// A pattern stroke is approximated: its warning comes back.
|
||||
let tile = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [tile]})).unwrap();
|
||||
s.execute("object.pattern.make", &json!({"name": "Dots", "width": 20, "height": 20})).unwrap();
|
||||
s.execute("object.pattern.done", &json!({})).unwrap();
|
||||
let frame = s.execute("shape.rectangle", &json!({"x": 20, "y": 20, "width": 50, "height": 40})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("paint.setStroke", &json!({"ids": [frame], "swatch": "Dots"})).unwrap();
|
||||
let pattern = |w: Vec<String>| w.iter().any(|w| w.contains("pattern strokes"));
|
||||
assert!(pattern(warnings(&export(&mut s, json!({})))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_pdf_writes_a_path() {
|
||||
let mut s = session(2);
|
||||
let dir = std::env::temp_dir().join(format!("vc-exportpdf-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("out.pdf").to_string_lossy().to_string();
|
||||
let v = export(&mut s, json!({"path": path, "range": "2"}));
|
||||
assert_eq!(v["path"], path.as_str());
|
||||
assert!(v.get("dataBase64").is_none());
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
assert_eq!(v["bytes"], bytes.len());
|
||||
assert_eq!(pages(&bytes), 1);
|
||||
assert_eq!(s.doc().unwrap().path, None, "exporting never retargets the document");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_list_the_pdf_options() {
|
||||
let mut s = Session::new();
|
||||
let v = s.execute("document.formats", &json!({})).unwrap();
|
||||
let pdf = v["formats"].as_array().unwrap().iter().find(|f| f["id"] == "pdf").unwrap();
|
||||
for k in ["artboard", "range", "preset", "standard", "compatibility", "compression", "security"] {
|
||||
assert!(pdf["options"].get(k).is_some(), "{k}");
|
||||
}
|
||||
assert_eq!(pdf["options"]["compatibility"]["default"], "1.7");
|
||||
assert_eq!(pdf["options"]["preset"]["default"], DEFAULT_PRESET);
|
||||
}
|
||||
@@ -210,6 +210,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(attributes::specs());
|
||||
v.extend(newart::specs());
|
||||
v.extend(colortheme::specs());
|
||||
v.extend(fileio::pdf::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -181,9 +181,18 @@ pub fn flatten_raster_effects(doc: &Document) -> Option<Document> {
|
||||
|
||||
/// PDF bytes for `doc`, with raster effects rendered as images.
|
||||
pub fn export_pdf(doc: &Document, opts: &vectorcraft_pdf::PdfOptions) -> std::result::Result<Vec<u8>, vectorcraft_pdf::PdfError> {
|
||||
export_pdf_with_report(doc, opts).map(|r| r.bytes)
|
||||
}
|
||||
|
||||
/// Like [`export_pdf`], also returning the writer's warnings (options not applied yet, features
|
||||
/// approximated or left out).
|
||||
pub fn export_pdf_with_report(
|
||||
doc: &Document,
|
||||
opts: &vectorcraft_pdf::PdfOptions,
|
||||
) -> std::result::Result<vectorcraft_pdf::ExportReport, vectorcraft_pdf::PdfError> {
|
||||
match flatten_raster_effects(doc) {
|
||||
Some(d) => vectorcraft_pdf::export(&d, opts),
|
||||
None => vectorcraft_pdf::export(doc, opts),
|
||||
Some(d) => vectorcraft_pdf::export_with_report(&d, opts),
|
||||
None => vectorcraft_pdf::export_with_report(doc, opts),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user