mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
Both writers now ask the shared stroke geometry how to write each stroke (effects::stroke::for_writer): a plain stroke where that is exact, else the canvas's filled outlines (arrowheads, width profiles, dashes fitted to corners, dots with round or projecting caps), grouped under the stroke opacity when the line and its heads overlap. Brushed strokes are written as their brush art. Inside/outside alignment only applies to closed paths, so open paths export as centred strokes. The PDF outside-stroke clip frame and the SVG outside mask now cover the whole stroke reach (miter spikes, projecting caps). SVG stroke gradients use the same paint box as the canvas. The svg crate docs no longer claim arrowheads, profiles, brushes and effects are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
3.5 KiB
Rust
114 lines
3.5 KiB
Rust
//! VectorCraft PDF export and import.
|
|
//!
|
|
//! - [`export`] writes one PDF page per artboard with `krilla`: vector paths (fills, strokes with
|
|
//! caps/joins/miter/dashes, inside/outside alignment as clips, non-zero/even-odd; arrowheads,
|
|
//! width profiles, fitted or dotted dashes as the canvas's filled outlines and brushed strokes
|
|
//! as their brush art), opacity and blend modes (transparency groups),
|
|
//! clip groups, linear/radial gradients (shadings), embedded images and text as outlined glyph
|
|
//! paths. Hidden objects, guides and template layers are skipped.
|
|
//! - [`import`] reads PDF (and PDF-compatible `.ai`) pages with `hayro-interpret` into a
|
|
//! [`Document`]: one artboard and one layer per page, paths with fill/stroke, clip groups,
|
|
//! transparency groups, axial/radial shadings → gradients, images (JPEG passthrough, others
|
|
//! re-encoded as PNG) and text as glyph outlines.
|
|
#![forbid(unsafe_code)]
|
|
|
|
mod export;
|
|
mod import;
|
|
|
|
pub use export::{export, export_with_report};
|
|
pub use import::{import, import_with_report};
|
|
|
|
use vectorcraft_doc::Document;
|
|
|
|
/// PDF standard / version to target.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub enum Compatibility {
|
|
/// PDF 1.4.
|
|
Pdf14,
|
|
/// PDF 1.5.
|
|
Pdf15,
|
|
/// PDF 1.6.
|
|
Pdf16,
|
|
/// PDF 1.7. The default.
|
|
#[default]
|
|
Pdf17,
|
|
/// PDF 2.0.
|
|
Pdf20,
|
|
/// PDF/A-2b (archival; validated by krilla).
|
|
PdfA2b,
|
|
/// PDF/X-4 — not supported by the writer yet; export returns [`PdfError::Unsupported`].
|
|
PdfX4,
|
|
}
|
|
|
|
/// Export options.
|
|
#[derive(Clone, Debug)]
|
|
pub struct PdfOptions {
|
|
/// 0-based artboard indices to export, in page order; `None` = all artboards.
|
|
pub artboards: Option<Vec<usize>>,
|
|
pub compatibility: Compatibility,
|
|
/// Compress content streams (Flate).
|
|
pub compress: bool,
|
|
/// Document title for the metadata; `None` = the document's title.
|
|
pub title: Option<String>,
|
|
/// Creation date as Unix seconds (UTC); `None` = now (native) / omitted (wasm). PDF/A needs a date.
|
|
pub created: Option<i64>,
|
|
}
|
|
|
|
impl Default for PdfOptions {
|
|
fn default() -> Self {
|
|
Self { artboards: None, compatibility: Compatibility::Pdf17, compress: true, title: None, created: None }
|
|
}
|
|
}
|
|
|
|
/// Import options.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ImportOptions {
|
|
/// Import at most this many pages (from the first); `None` = all pages.
|
|
pub max_pages: Option<usize>,
|
|
/// Horizontal gap in points between the artboards created for consecutive pages.
|
|
pub artboard_gap: f64,
|
|
}
|
|
|
|
impl Default for ImportOptions {
|
|
fn default() -> Self {
|
|
Self { max_pages: None, artboard_gap: 36.0 }
|
|
}
|
|
}
|
|
|
|
/// Result of an import with non-fatal warnings (unsupported features, skipped content).
|
|
#[derive(Clone, Debug)]
|
|
pub struct ImportReport {
|
|
pub document: Document,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
/// Result of an export with non-fatal warnings (features approximated or dropped).
|
|
#[derive(Clone, Debug)]
|
|
pub struct ExportReport {
|
|
pub bytes: Vec<u8>,
|
|
pub warnings: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error, PartialEq)]
|
|
pub enum PdfError {
|
|
#[error("the document has no artboards")]
|
|
NoArtboards,
|
|
#[error("artboard {0} does not exist")]
|
|
BadArtboard(usize),
|
|
#[error("unsupported: {0}")]
|
|
Unsupported(String),
|
|
#[error("PDF writer error: {0}")]
|
|
Write(String),
|
|
#[error("cannot read PDF: {0}")]
|
|
Parse(String),
|
|
#[error("the PDF has no pages")]
|
|
NoPages,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
#[cfg(test)]
|
|
mod tests_stroke;
|
|
#[cfg(test)]
|
|
mod tests_strokeout;
|