mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 06:31:41 +00:00
With downsampling on, each piece of an image bent by a Warp effect was resampled for its own placed size, so a warp wrote one image XObject for each size a piece was placed at (four for a 4 x 4 warp that stretches across, eight for a colorized one with its soft masks). The pieces are now resampled for the widest and the highest size any piece places the image at, and all paint that one image. Pieces off the page count too, and another placement of the same image keeps its own size. Without downsampling, a colorized image and a CMYK image with a grid mask no longer key their cache by placed size, so each is decoded and encoded once for all the sizes it is placed at, as plain images are.
175 lines
8.1 KiB
Rust
175 lines
8.1 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};
|
|
use krilla::image::Image;
|
|
use krilla::num::NormalizedF32;
|
|
use krilla::page::PageSettings;
|
|
use krilla::paint::{Fill, LinearGradient, RadialGradient, SpreadMethod, Stop, Stroke, StrokeDash, SweepGradient};
|
|
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::surface::Surface;
|
|
use crate::{Compatibility, ExportReport, PdfError, PdfOptions, Standard};
|
|
|
|
mod entry;
|
|
mod exporter;
|
|
mod images;
|
|
mod links;
|
|
mod nodes;
|
|
mod paint;
|
|
mod text;
|
|
mod writer;
|
|
|
|
pub(crate) use entry::{RgbInCmyk, leaving_out_undecodable};
|
|
pub use entry::{check_page_sizes, export, export_with_report, page_areas};
|
|
#[cfg(test)]
|
|
use exporter::{MAX_COORD, writable, xf};
|
|
#[cfg(test)]
|
|
use images::{png_8_bit, png_16_bit};
|
|
#[cfg(test)]
|
|
pub(crate) use nodes::MEASURES;
|
|
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>,
|
|
/// While the pieces of a mesh-warped image are written: the size its pixels are resampled for
|
|
/// ([`Exporter::warp_image_size`]).
|
|
warp_size: Option<images::SharedSize>,
|
|
/// 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,
|
|
/// Conic gradients may be written as sweep shadings, whose PostScript calculator functions
|
|
/// PDF/A forbids; otherwise they are drawn as images ([`Self::area_paint`]).
|
|
sweeps: 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,
|
|
/// The paper of the page being drawn, in its drawing space: the box of each layer's form
|
|
/// covers it ([`crate::forms::layer_form`]).
|
|
paper: Rect,
|
|
/// 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,
|
|
/// The most [`Self::symbol_nodes`] may reach: [`vectorcraft_doc::MAX_SYMBOL_NODES`], or less
|
|
/// while one of the copies on a sheet is drawn ([`Writer::page`]).
|
|
pub(crate) symbol_limit: 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 = Surface::page(page.surface(), 100.0);
|
|
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());
|
|
}
|
|
}
|