use super::writer::{Sheet, Writer, date_time}; use super::{Affine, Document, ExportReport, Exporter, HashSet, PageBoxes, PdfError, PdfOptions, Rect, write}; /// Export `doc` as PDF bytes: one page per artboard (or the artboards chosen in `opts`). pub fn export(doc: &Document, opts: &PdfOptions) -> Result, PdfError> { export_with_report(doc, opts).map(|r| r.bytes) } /// Like [`export`], also returning warnings: options accepted but not applied yet (see /// [`crate::PdfSettings::warnings`]) and features approximated or dropped. pub fn export_with_report(doc: &Document, opts: &PdfOptions) -> Result { write::guard(|| leaving_out_undecodable(doc, |left_out| export_with_report_inner(doc, opts, left_out))) } /// Run `write` (a whole PDF written by a [`Writer`]), and run it again each time it fails on an /// image whose pixel data the writer couldn't decode, with that image left out: [`Writer::finish`] /// adds the image's key to the set `write` is given, and the exporters leave out the images it /// holds. The writer reports such an image (a PNG, GIF or WebP kept as it is, whose header reads) /// only when it finishes the file. Each repeated run leaves out at least one more image. pub(crate) fn leaving_out_undecodable(doc: &Document, mut write: impl FnMut(&mut HashSet) -> Result) -> Result { // The most runs repeated: each is a whole export, and a document of many images that don't // decode would make them quadratic. Past it, one last run leaves out every image of `doc`, so // the export still finishes (the images left out are reported). const MAX_RETRIES: usize = 16; let mut left_out = HashSet::new(); let mut retries = 0; loop { let known = left_out.len(); match write(&mut left_out) { Err(_) if left_out.len() > known && retries < MAX_RETRIES => retries += 1, Err(_) if left_out.len() > known && retries == MAX_RETRIES => { left_out.extend(doc.images.keys().cloned()); retries += 1; } done => return done, } } } fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut HashSet) -> Result { let set = &opts.settings; set.check()?; vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; // Live geometry effects export as their result; raster effects are reported below. let baked = vectorcraft_effects::bake_document(doc); // Baking stops early when cancelled, leaving a half-baked copy that must not be written. vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; let doc = baked.as_ref().unwrap_or(doc); let pages = pages(doc, opts)?; let title = crate::pdfx::title(set.standard, opts.title.clone().unwrap_or_else(|| doc.title.clone())); // The time of writing (none on the web) dates the editing data and the page information. let now = vectorcraft_doc::metadata::output_time(); let created_at = opts.created.or(now); crate::pdfx::require_date(set.standard, created_at)?; let display_doc_title = opts.display_doc_title || opts.title.as_deref().is_some_and(|title| !title.trim().is_empty()); let mut w = Writer::new(doc, set, &title, created_at, doc.color_mode == vectorcraft_doc::ColorMode::Cmyk)?; // Preserve Editing: the native document as an embedded file (`check` refused PDF/A with it). let mut warnings = set.warnings(); let native = opts.native.as_deref().filter(|_| set.preserve_editing); if let Some(native) = native { w.pdf.document_mut().embed_file(crate::editing::embedded_file(native, set.compression.compress_text, now.map(date_time))); } else if set.preserve_editing { warnings.push("Preserve editing needs the native document, which wasn't given: the PDF reopens as plain artwork".into()); } let mut ex = Exporter::new(doc, set); ex.non_printing = set.include_non_printing || set.create_layers; ex.out = w.out.clone(); ex.left_out = left_out.clone(); // A knockout page group draws the layers' objects one by one, not layer by layer. ex.layers = set.writes_layers() && !doc.page_knockout; if set.writes_layers() && doc.page_knockout { warnings.push("PDF layers can't be written with a knockout page group: every layer is plain page content".into()); } ex.overprint = set.advanced.overprint == crate::Overprint::Preserve; let bleed = set.bleed_of(doc); let marks = set.marks.printer_marks(); // In PDF 1.6 and later files that aren't PDF/X files, a page with a side longer than // [`MAX_PAGE_SIDE`] is written in larger units; other files have every page in points. let scales = set.compatibility.has_user_unit() && !set.standard.is_pdfx(); let mut units = Vec::with_capacity(pages.len()); let total = pages.len() as u64; for (written, (i, boxes)) in pages.into_iter().enumerate() { // The page count and the page being written, for a client that asked for progress. vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; vectorcraft_doc::cancel::progress(written as u64 + 1, total, &format!("Writing page {} of {total}", written + 1)); let (m, r) = (boxes.media, boxes.bleed); let info = if marks.page_info { crate::marks::page_info(doc, &title, i, now) } else { String::new() }; let art = crate::marks::art(doc, &marks, &boxes, bleed, &info); // The page is the media box, y down from its top-left corner (like ours), in units of // `unit` points. let unit = if scales { user_unit(m.width(), m.height()) } else { 1.0 }; units.push(unit); let shift = Affine::translate((-m.x0, -m.y0)); let view = if unit > 1.0 { Affine::scale(1.0 / unit) * shift } else { shift }; let sheet = Sheet { size: (m.width() / unit, m.height() / unit), unit, trim: view.transform_rect_bbox(boxes.trim), bleed: view.transform_rect_bbox(r), view, window: None, place: Affine::IDENTITY, area: r, // Art reaches as far as the bleed: the marks lie outside it. clip: m != r, marks: art.as_ref(), negative: false, }; w.page(&mut ex, &sheet).map_err(|_| PdfError::BadArtboard(i))?; } vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?; let (layers, overprinted, invisible) = (ex.layers, ex.overprinted, ex.invisible); w.absorb(ex); let (bytes, more) = w.finish(left_out, display_doc_title)?; let bytes = crate::forms::finish(bytes, doc, set, layers, overprinted, invisible)?; // The writer gives the file its creation date as its modification date too. let bytes = match opts.modified.filter(|m| Some(*m) != created_at) { Some(modified) => crate::post::modification_date(bytes, modified)?, None => bytes, }; // The writer's metadata has no copyright: it is added to the XMP packet. let bytes = crate::post::rights(bytes, &doc.metadata)?; warnings.extend(more); let bytes = crate::post::user_units(bytes, &units)?; let bytes = crate::post::finish(bytes, opts, native.is_some(), &mut warnings)?; warnings.dedup(); Ok(ExportReport { bytes, warnings }) } /// The artboards `opts` exports, in page order, with the boxes of their pages: Marks and Bleeds /// make each page its artboard (the trim box) grown by the bleed, and by the printer's marks /// around that. fn pages(doc: &Document, opts: &PdfOptions) -> Result, PdfError> { let indices: Vec = match &opts.artboards { Some(v) => v.clone(), None => (0..doc.artboards.len()).collect(), }; if doc.artboards.is_empty() || indices.is_empty() { return Err(PdfError::NoArtboards); } let (bleed, marks) = (opts.settings.bleed_of(doc), opts.settings.marks.printer_marks()); indices.into_iter().map(|i| Ok((i, PageBoxes::new(doc.artboards.get(i).ok_or(PdfError::BadArtboard(i))?.rect, bleed, &marks)))).collect() } /// What each page of `doc` exported with `opts` shows, in document space (its media box: the /// artboard with the bleed and the printer's marks), in page order. pub fn page_areas(doc: &Document, opts: &PdfOptions) -> Result, PdfError> { Ok(pages(doc, opts)?.into_iter().map(|(_, b)| b.media).collect()) } /// The largest side of a page in units of its default user space that ISO 32000-1 (Annex C.2) /// gives: 14,400 (200 inches in units of 1/72 inch). pub(crate) const MAX_PAGE_SIDE: f64 = 14_400.0; /// The `UserUnit` (PDF 1.6) of a page `w` × `h` points large: 1 when both sides are at most /// [`MAX_PAGE_SIDE`], else the longer side divided by it, rounded up to the next thousandth, so /// that both sides are at most [`MAX_PAGE_SIDE`] units. fn user_unit(w: f64, h: f64) -> f64 { let side = w.abs().max(h.abs()); if side.is_finite() && side > MAX_PAGE_SIDE { (side / MAX_PAGE_SIDE * 1000.0).ceil() / 1000.0 } else { 1.0 } } /// The RGB colours and non-CMYK images a CMYK document carries, which the export separates into /// CMYK. Their darkest values come out as rich blacks, so the export says how many there are. #[derive(Default)] pub(crate) struct RgbInCmyk { /// Distinct RGB colours, by the bits of their channels. pub(super) colours: HashSet<[u32; 3]>, /// Keys of the raster images that aren't CMYK. pub(super) images: HashSet, } impl RgbInCmyk { pub(super) fn merge(&mut self, other: RgbInCmyk) { self.colours.extend(other.colours); self.images.extend(other.images); } /// The warning for the export report; `None` when the document has nothing of the sort. pub(super) fn warning(&self) -> Option { let (c, i) = (self.colours.len(), self.images.len()); if c == 0 && i == 0 { return None; } let mut parts = vec![]; if c > 0 { parts.push(format!("{c} RGB colour{}", if c == 1 { "" } else { "s" })); } if i > 0 { parts.push(format!("{i} RGB or grey image{}", if i == 1 { "" } else { "s" })); } Some(format!( "this CMYK document has {}: they are converted to CMYK through the colour settings, so dark ones can come out as rich blacks on several plates; paint them with CMYK colours (Edit › Edit Colors › Convert to CMYK) for pure black ink", parts.join(" and ") )) } }