mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
An unclipped opacity mask is drawn on a white backdrop. When an image in the mask art could not be written (a colorized image whose color channel is over the size limit, an image that can't be decoded, or one placed beyond the coordinates a file holds), the backdrop stayed white and the masked object showed in full where the image should hide it. The exporter now records an image left out while it writes mask art, and leaves the masked object out instead of applying the incomplete mask, with a warning. A failure in a nested mask makes the outer mask incomplete too.
160 lines
7.3 KiB
Rust
160 lines
7.3 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>>,
|
|
/// `Some` while opacity mask art is written: whether an image in it was left out.
|
|
image_left_out: Option<bool>,
|
|
/// 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());
|
|
}
|
|
}
|