Files
vectorcraft/crates/pdf/src/export.rs
T

158 lines
7.2 KiB
Rust

//! Document → PDF (krilla). Mirrors the tree walk of `vectorcraft-render`.
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use craft_pdf::write::backend as krilla;
use craft_pdf::write::{self, Metadata};
use krilla::color::separation::{Color as SepColor, SeparationColorant, SeparationSpace};
use krilla::color::{cmyk, luma, rgb};
use krilla::configure::{Archival, PdfVersion};
use krilla::geom::{Path, PathBuilder, Size, Transform};
use krilla::image::Image;
use krilla::num::NormalizedF32;
use krilla::page::PageSettings;
use krilla::paint::{Fill, LinearGradient, RadialGradient, SpreadMethod, Stop, Stroke, StrokeDash};
use krilla::surface::Surface;
use krilla::text::{Glyph, GlyphId, KrillaGlyph};
use kurbo::{PathEl, Shape, Vec2};
use vectorcraft_color::{BlendMode, Color, GradientKind, Paint};
use vectorcraft_doc::{AppearanceItem, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, SymbolBoxes, TextObject};
use vectorcraft_effects::stroke::{self, WrittenShape};
use vectorcraft_geom::{Affine, BezPath, FillRule, Rect};
use vectorcraft_text::embed::Embedding;
use vectorcraft_text::{ListMarker, TextLayout};
use crate::forms::Mark;
use crate::lab_spot::{find, rfind};
use crate::marks::PageBoxes;
use crate::output::ColorOut;
use crate::{Compatibility, ExportReport, PdfError, PdfOptions, Standard};
mod entry;
mod exporter;
mod images;
mod nodes;
mod paint;
mod text;
mod writer;
pub(crate) use entry::{RgbInCmyk, leaving_out_undecodable};
pub use entry::{export, export_with_report, page_areas};
#[cfg(test)]
use exporter::{MAX_COORD, writable, xf};
#[cfg(test)]
use images::{png_8_bit, png_16_bit};
pub(crate) use writer::{Sheet, Writer};
pub(crate) struct Exporter<'a> {
doc: &'a Document,
/// The art boxes of `doc`'s symbols, which the walk measures symbol instances by when it
/// leaves out objects outside the page ([`SymbolBoxes::quick`]). The box of an instance whose
/// art holds brushed strokes grows by as far as their brush art can paint
/// ([`SymbolBoxes::with_brush_art`]).
symbols: SymbolBoxes<'a>,
warnings: Vec<String>,
images: HashMap<String, Option<Image>>,
/// The brush library, parsed when the first brushed stroke is written.
brushes: Option<Vec<vectorcraft_brush::Brush>>,
/// Whether the group being written is a knockout group (what its neutral children inherit).
knockout: bool,
/// Spot colours written with a Lab alternate ([`crate::lab_spot`]): colorant name, Lab values.
lab_spots: Vec<(String, vectorcraft_color::cms::Lab)>,
/// Images may ask viewers to smooth them (not in PDF/A).
interpolate: bool,
/// Images may have 16 bits a sample (PDF 1.5 and later); in earlier files they are written at
/// 8 bits ([`embed`]).
sixteen_bit: bool,
/// How images are resampled and compressed.
compression: &'a crate::CompressionSettings,
/// Layers whose Print option is off are written too.
pub non_printing: bool,
/// How colours are separated into CMYK (the colour settings' intent unless print sets one).
pub intent: vectorcraft_color::cms::Intent,
/// Whether a layer shows transparency (found out on the first page that asks).
transparent: Option<bool>,
/// How colours are written (the Output settings; print writes them as they are).
pub out: Arc<ColorOut>,
/// Colours are converted to the destination (printer's marks aren't).
convert: bool,
/// RGB colours and images of a CMYK document drawn so far.
rgb_in_cmyk: RgbInCmyk,
/// Text as glyph outlines; else the characters' fills are real text in embedded fonts.
outline_text: bool,
/// The fonts real text is written in, by face id, with their units per em; `None` for faces
/// that can't be embedded.
fonts: HashMap<u32, Option<(krilla::text::Font, f64)>>,
/// Layers and sublayers are drawn as forms marked for their optional content groups
/// ([`crate::forms`]).
pub layers: bool,
/// Each layer's index in [`crate::forms::pdf_layers`], by address.
layer_index: HashMap<usize, usize>,
/// The address of the layer whose form is being drawn.
in_layer: usize,
/// Overprinting fills and strokes are drawn as forms marked to overprint.
pub overprint: bool,
/// An overprinting fill or stroke was drawn so.
pub overprinted: bool,
/// Characters with neither fill nor stroke, or whose fill is drawn as art (outlines filled
/// with an image or pattern), were written as text in forms marked invisible.
pub invisible: bool,
/// Inline graphics being drawn inside inline graphics (text in a symbol shown inline in
/// text…): deeper ones are left out.
inline_depth: u32,
/// Tiling patterns being drawn inside tiling patterns ([`Self::tiling`]).
pattern_depth: u32,
/// Keys of the images left out because the writer couldn't decode them
/// ([`leaving_out_undecodable`]).
pub left_out: HashSet<String>,
/// Objects the symbol instances written held, together
/// ([`vectorcraft_doc::MAX_SYMBOL_NODES`]).
symbol_nodes: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writable_bounds_values_the_transform_and_values_through_it() {
let mut pdf = krilla::Document::new();
let mut page = pdf.start_page_with(PageSettings::new(Size::from_wh(100.0, 100.0).unwrap()));
let mut s = page.surface();
let r = Rect::new(0.0, 0.0, 10.0, 10.0);
assert!(writable(&s, Affine::IDENTITY, r));
assert!(!writable(&s, Affine::IDENTITY, Rect::new(-2_147_483_649.0, 0.0, 10.0, 10.0)));
assert!(!writable(&s, Affine::IDENTITY, Rect::new(f64::NAN, 0.0, 10.0, 10.0)));
assert!(!writable(&s, Affine::translate((0.0, 2e9)), r));
assert!(!writable(&s, Affine::scale(2e9), Rect::ZERO));
// `writable` also checks the surface's own transform: with the surface moved to
// x = MAX_COORD, a point at the origin fits and a 10 by 10 box does not.
s.push_transform(&xf(Affine::translate((MAX_COORD, 0.0))));
assert!(writable(&s, Affine::IDENTITY, Rect::ZERO));
assert!(!writable(&s, Affine::IDENTITY, r));
s.pop();
s.finish();
page.finish();
}
#[test]
fn a_16_bit_png_at_8_bits_keeps_its_color_type_and_icc_profile() {
use image::{ImageDecoder, ImageEncoder};
let icc = vectorcraft_color::cms::icc_bytes(vectorcraft_color::cms::GRAY).unwrap().to_vec();
let gray = image::DynamicImage::ImageLuma16(image::ImageBuffer::from_fn(4, 2, |x, y| image::Luma([x as u16 * 20000 + y as u16])));
let mut png = vec![];
let mut encoder = image::codecs::png::PngEncoder::new(&mut png);
encoder.set_icc_profile(icc.clone()).unwrap();
gray.write_with_encoder(encoder).unwrap();
assert!(png_16_bit(&png));
let out = png_8_bit(&png).unwrap();
assert!(!png_16_bit(&out));
let mut decoder = image::codecs::png::PngDecoder::new(std::io::Cursor::new(out.as_slice())).unwrap();
assert_eq!(decoder.icc_profile().unwrap(), Some(icc));
assert_eq!(decoder.color_type(), image::ColorType::L8);
assert_eq!(image::DynamicImage::from_decoder(decoder).unwrap().to_luma8(), gray.to_luma8());
}
}