effectcraft/crates/pdf/src/lib.rs

195 lines
6.5 KiB
Rust

//! Vector footage: PDF, PDF-compatible Illustrator files (`.ai`) and Encapsulated PostScript
//! (see README.md for what is read and the specifications used).
//!
//! Files parse into the same render tree as SVG ([`effectcraft_svg::Doc`]): document pixels are
//! PostScript points (1/72 in), y down, so footage rasterises at any scale (Continuously
//! Rasterize) and converts to shape layers (Create Shapes from Vector Layer). The root group's
//! children are the document's **layers**: one group per top-level optional-content group
//! (Illustrator layers), with content outside them gathered into groups of its own.
mod build;
mod cff;
mod color;
mod encoding;
mod font;
mod image;
mod object;
mod page;
mod ps;
mod type1;
pub mod write;
pub use effectcraft_svg::Doc;
use effectcraft_svg::{Affine, Group, Node};
#[derive(Debug, Clone, PartialEq)]
pub enum Error {
NotVector,
Encrypted,
NoPages,
PageOutOfRange(usize),
NoBoundingBox,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::NotVector => write!(f, "not a PDF, Illustrator or EPS file"),
Error::Encrypted => write!(f, "encrypted PDF files are not supported"),
Error::NoPages => write!(f, "the PDF has no pages"),
Error::PageOutOfRange(n) => write!(f, "the PDF has no page {}", n + 1),
Error::NoBoundingBox => write!(f, "the EPS file has no %%BoundingBox"),
}
}
}
impl std::error::Error for Error {}
/// File extensions read as vector footage by this crate.
pub const EXTENSIONS: &[&str] = &["pdf", "ai", "eps", "epsf", "epsi"];
/// The format of a file, by its first bytes.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Format {
Pdf,
Eps,
}
/// Sniff a PDF (`%PDF-` near the start, as PDF-compatible `.ai` files are) or an EPS file.
pub fn sniff(bytes: &[u8]) -> Option<Format> {
let head = &bytes[..bytes.len().min(1024)];
if head.windows(5).any(|w| w == b"%PDF-") {
return Some(Format::Pdf);
}
if bytes.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) || bytes.starts_with(b"%!PS") {
return Some(Format::Eps);
}
None
}
/// A footage codec name for a sniffed file ("PDF", "AI", "EPS").
pub fn codec(path: &str, bytes: &[u8]) -> Option<&'static str> {
let f = sniff(bytes)?;
let ai = path.to_ascii_lowercase().ends_with(".ai");
Some(match (f, ai) {
(_, true) => "AI",
(Format::Pdf, false) => "PDF",
(Format::Eps, false) => "EPS",
})
}
/// Parse the first page of a PDF / `.ai`, or an EPS file.
pub fn parse(bytes: &[u8]) -> Result<Doc, Error> {
parse_page(bytes, 0)
}
/// Parse page `index` of a PDF (EPS files have one page).
pub fn parse_page(bytes: &[u8], index: usize) -> Result<Doc, Error> {
match sniff(bytes) {
Some(Format::Pdf) => parse_pdf(bytes, index),
Some(Format::Eps) => parse_eps(bytes),
None => Err(Error::NotVector),
}
}
/// Number of pages (1 for EPS).
pub fn page_count(bytes: &[u8]) -> usize {
match sniff(bytes) {
Some(Format::Pdf) => page::pages(&object::File::parse(bytes)).len(),
Some(Format::Eps) => 1,
None => 0,
}
}
/// Build the document: `root` children become layer groups (see the crate docs).
fn finish(nodes: Vec<Node>, flags: Vec<bool>, skipped: Vec<String>, width: f64, height: f64, to_doc: Affine) -> Doc {
let mut layers: Vec<Node> = vec![];
let mut loose: Vec<Node> = vec![];
let flush = |loose: &mut Vec<Node>, layers: &mut Vec<Node>| {
if !loose.is_empty() {
let mut g = build::group("");
g.children = std::mem::take(loose);
layers.push(Node::Group(g));
}
};
for (n, is_layer) in nodes.into_iter().zip(flags.into_iter().chain(std::iter::repeat(false))) {
if is_layer {
flush(&mut loose, &mut layers);
layers.push(n);
} else {
loose.push(n);
}
}
flush(&mut loose, &mut layers);
// Name unnamed layers "Layer N" (bottom first, as Illustrator numbers them).
for (i, l) in layers.iter_mut().enumerate() {
if let Node::Group(g) = l
&& g.name.is_empty()
{
g.name = format!("Layer {}", i + 1);
}
}
let mut root = Group::new("page");
root.transform = to_doc;
root.children = layers;
Doc { width, height, root, skipped }
}
fn parse_pdf(bytes: &[u8], index: usize) -> Result<Doc, Error> {
let file = object::File::parse(bytes);
if file.encrypted() {
return Err(Error::Encrypted);
}
let pages = page::pages(&file);
if pages.is_empty() {
return Err(Error::NoPages);
}
let pg = pages.get(index).ok_or(Error::PageOutOfRange(index))?;
let [x0, y0, x1, y1] = pg.bbox;
let (w, h) = (x1 - x0, y1 - y0);
// Default user space (y up) → document pixels (y down), with the page's /Rotate.
let (to_doc, dw, dh) = match pg.rotate {
90 => (Affine::new([0.0, 1.0, 1.0, 0.0, -y0, -x0]), h, w),
180 => (Affine::new([-1.0, 0.0, 0.0, 1.0, x1, -y0]), w, h),
270 => (Affine::new([0.0, -1.0, -1.0, 0.0, y1, x1]), h, w),
_ => (Affine::new([1.0, 0.0, 0.0, -1.0, -x0, y1]), w, h),
};
let content = page::page_content(&file, pg);
let mut it = page::Interp::new(&file, pg.bbox);
it.run(&content, &pg.resources, Affine::IDENTITY, 0);
let (nodes, flags, skipped) = it.b.finish();
Ok(finish(nodes, flags, skipped, dw.max(1.0), dh.max(1.0), to_doc))
}
fn parse_eps(bytes: &[u8]) -> Result<Doc, Error> {
let (ps, bb) = ps::eps_parts(bytes).ok_or(Error::NoBoundingBox)?;
let mut it = ps::Interp::new(bb);
it.run(ps);
let (nodes, flags, skipped) = it.b.finish();
let to_doc = Affine::new([1.0, 0.0, 0.0, -1.0, -bb[0], bb[3]]);
Ok(finish(nodes, flags, skipped, (bb[2] - bb[0]).max(1.0), (bb[3] - bb[1]).max(1.0), to_doc))
}
/// The document's layer names (the root's children), bottom first.
pub fn layer_names(doc: &Doc) -> Vec<String> {
doc.root
.children
.iter()
.map(|n| match n {
Node::Group(g) => g.name.clone(),
Node::Shape(s) => s.name.clone(),
Node::Image(i) => i.name.clone(),
})
.collect()
}
/// The document with only layer `index` (for one layer of a file imported as a composition).
pub fn layer_doc(doc: &Doc, index: usize) -> Doc {
let mut d = doc.clone();
d.root.children = doc.root.children.get(index).cloned().into_iter().collect();
d
}
#[cfg(test)]
mod tests;