vectorcraft/crates/pdf/src/settings.rs

494 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Save PDF settings: the serde model a preset stores and `document.exportPdf` takes (camelCase
//! JSON; every field is optional and falls back to its default), the checks that refuse settings
//! the writer can't honour, and the warnings for options it accepts but doesn't apply yet.
use serde::{Deserialize, Serialize};
use crate::PdfError;
/// A closed list of choices (a settings enum), for UIs that show it as a dropdown.
pub trait Choice: Copy + 'static {
/// Every choice, in display order.
const ALL: &'static [Self];
/// The JSON ids, in display order.
const IDS: &'static [&'static str];
/// The labels shown in the PDF dialogs, in display order.
const LABELS: &'static [&'static str];
}
/// A closed list of choices: a serde enum (its JSON ids) with a UI label per variant.
macro_rules! choice {
($(#[$m:meta])* $name:ident { $($(#[$vm:meta])* $v:ident = $id:literal, $label:literal;)+ } default $d:ident) => {
$(#[$m])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum $name {
$($(#[$vm])* #[serde(rename = $id)] $v,)+
}
impl $crate::Choice for $name {
const ALL: &'static [Self] = &[$(Self::$v),+];
const IDS: &'static [&'static str] = &[$($id),+];
const LABELS: &'static [&'static str] = &[$($label),+];
}
impl $name {
/// The label shown in the PDF dialogs.
pub fn label(self) -> &'static str {
match self {
$(Self::$v => $label,)+
}
}
/// The JSON id.
pub fn id(self) -> &'static str {
match self {
$(Self::$v => $id,)+
}
}
}
impl Default for $name {
fn default() -> Self {
Self::$d
}
}
};
}
pub(crate) use choice;
choice! {
/// The PDF standard the file conforms to.
Standard {
None = "none", "None";
/// PDF/A-2b (archival), validated by the writer.
PdfA2b = "pdfA2b", "PDF/A-2b";
PdfX1a = "pdfX1a", "PDF/X-1a:2001";
PdfX3 = "pdfX3", "PDF/X-3:2002";
PdfX4 = "pdfX4", "PDF/X-4:2010";
} default None
}
impl Standard {
/// A file of this standard can declare PDF version `c`: PDF/A-2 is based on PDF 1.7 (so it
/// can't be PDF 2.0), PDF/X-4 on PDF 1.6, and PDF/X-1a and PDF/X-3 on PDF 1.3, which the
/// writer writes as 1.4 (the file says 1.3, see [`crate::pdfx`]).
pub fn allows(self, c: Compatibility) -> bool {
use Compatibility::*;
match self {
Self::None => true,
Self::PdfA2b => c != Pdf20,
Self::PdfX1a | Self::PdfX3 => c == Pdf14,
Self::PdfX4 => matches!(c, Pdf14 | Pdf15 | Pdf16),
}
}
/// The latest PDF version a file of this standard can declare (what choosing it sets).
pub fn version(self) -> Compatibility {
match self {
Self::None | Self::PdfA2b => Compatibility::Pdf17,
Self::PdfX1a | Self::PdfX3 => Compatibility::Pdf14,
Self::PdfX4 => Compatibility::Pdf16,
}
}
}
choice! {
/// The PDF version written in the file header.
Compatibility {
Pdf14 = "1.4", "PDF 1.4";
Pdf15 = "1.5", "PDF 1.5";
Pdf16 = "1.6", "PDF 1.6";
Pdf17 = "1.7", "PDF 1.7";
Pdf20 = "2.0", "PDF 2.0";
} default Pdf17
}
choice! {
/// How images above the threshold resolution are resampled.
Downsample {
None = "none", "No downsampling";
Average = "average", "Average";
Subsample = "subsample", "Subsample";
Bicubic = "bicubic", "Bicubic";
} default None
}
choice! {
/// Compression of colour and greyscale images.
ImageCodec {
None = "none", "None";
Zip = "zip", "ZIP";
Jpeg = "jpeg", "JPEG";
Jpeg2000 = "jpeg2000", "JPEG 2000";
/// JPEG images stay JPEG, the others are compressed losslessly.
Auto = "auto", "Automatic";
} default Auto
}
choice! {
/// JPEG image quality.
JpegQuality {
Minimum = "minimum", "Minimum";
Low = "low", "Low";
Medium = "medium", "Medium";
High = "high", "High";
Maximum = "maximum", "Maximum";
} default Maximum
}
choice! {
/// Compression of 1-bit (monochrome) images.
MonoCodec {
None = "none", "None";
CcittG3 = "ccittG3", "CCITT Group 3";
CcittG4 = "ccittG4", "CCITT Group 4";
Zip = "zip", "ZIP";
RunLength = "runLength", "Run Length";
} default Zip
}
choice! {
/// The style of printer's marks.
MarkKind {
Roman = "roman", "Roman";
Japanese = "japanese", "Japanese";
} default Roman
}
choice! {
/// Colour conversion on output.
ColorConversion {
None = "none", "No conversion";
Destination = "destination", "Convert to destination";
/// Convert only colours whose profile differs from the destination's.
PreserveNumbers = "preserveNumbers", "Convert to destination (preserve numbers)";
} default None
}
choice! {
/// Which ICC profiles the file embeds.
ProfileInclusion {
None = "none", "Don't include profiles";
All = "all", "Include all profiles";
Destination = "destination", "Include destination profile";
TaggedSource = "taggedSource", "Include tagged source profiles";
} default None
}
choice! {
/// What happens to overprinting fills and strokes.
Overprint {
Preserve = "preserve", "Preserve";
Discard = "discard", "Discard";
} default Preserve
}
choice! {
/// Printing allowed by the permissions.
Printing {
None = "none", "None";
Low = "low", "Low resolution (150 ppi)";
High = "high", "High resolution";
} default High
}
choice! {
/// Changes allowed by the permissions.
Changes {
None = "none", "None";
Pages = "pages", "Inserting, deleting and rotating pages";
Forms = "forms", "Filling in form fields and signing";
Comments = "comments", "Commenting, filling in form fields and signing";
Any = "any", "Any except extracting pages";
} default Any
}
/// Save PDF settings (the dialog's sections; what a preset stores).
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PdfSettings {
pub standard: Standard,
pub compatibility: Compatibility,
// General.
/// Embed the native document so the PDF reopens editable.
pub preserve_editing: bool,
/// Embed a thumbnail image per page.
pub thumbnails: bool,
/// Write a linearised file that displays page by page while it downloads.
pub fast_web_view: bool,
/// The app opens the written file (frontends only; the writer ignores it).
pub view_after_saving: bool,
/// Write top-level layers as PDF layers (optional content).
pub create_layers: bool,
/// Keep the layers whose Print option is off (they are left out otherwise, unless
/// `create_layers` is on).
pub include_non_printing: bool,
pub compression: CompressionSettings,
pub marks: MarkSettings,
pub bleed: BleedSettings,
pub output: OutputSettings,
pub advanced: AdvancedSettings,
pub security: SecuritySettings,
}
/// Resampling and compression of one kind of image.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ImageCompression {
pub downsample: Downsample,
/// Target resolution (ppi).
pub ppi: f64,
/// Only images above this resolution (ppi) are downsampled.
pub above_ppi: f64,
pub compression: ImageCodec,
pub quality: JpegQuality,
}
impl Default for ImageCompression {
fn default() -> Self {
Self { downsample: Downsample::None, ppi: 300.0, above_ppi: 450.0, compression: ImageCodec::Auto, quality: JpegQuality::Maximum }
}
}
/// Resampling and compression of monochrome images.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MonoCompression {
pub downsample: Downsample,
pub ppi: f64,
pub above_ppi: f64,
pub compression: MonoCodec,
}
impl Default for MonoCompression {
fn default() -> Self {
Self { downsample: Downsample::None, ppi: 1200.0, above_ppi: 1800.0, compression: MonoCodec::Zip }
}
}
/// The Compression section.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct CompressionSettings {
pub color: ImageCompression,
pub gray: ImageCompression,
pub mono: MonoCompression,
/// Compress text and line art (Flate content streams).
pub compress_text: bool,
}
impl Default for CompressionSettings {
fn default() -> Self {
Self { color: ImageCompression::default(), gray: ImageCompression::default(), mono: MonoCompression::default(), compress_text: true }
}
}
/// Printer's marks (Marks and Bleeds section).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct MarkSettings {
pub trim: bool,
pub registration: bool,
pub color_bars: bool,
pub page_info: bool,
pub kind: MarkKind,
/// Trim mark weight (pt).
pub weight: f64,
/// Distance of the marks from the artboard (pt).
pub offset: f64,
}
impl Default for MarkSettings {
fn default() -> Self {
Self { trim: false, registration: false, color_bars: false, page_info: false, kind: MarkKind::Roman, weight: 0.25, offset: 6.0 }
}
}
impl MarkSettings {
/// Any mark is on.
pub fn any(&self) -> bool {
self.trim || self.registration || self.color_bars || self.page_info
}
}
/// Bleed (Marks and Bleeds section), in points.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct BleedSettings {
/// Use the document's bleed instead of the values below.
pub use_document: bool,
pub top: f64,
pub bottom: f64,
pub left: f64,
pub right: f64,
}
impl BleedSettings {
/// `[top, bottom, left, right]`.
pub(crate) fn values(&self) -> [f64; 4] {
[self.top, self.bottom, self.left, self.right]
}
}
/// The Output section.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct OutputSettings {
pub conversion: ColorConversion,
/// Destination profile name (empty: the document's profile).
pub destination: String,
pub profiles: ProfileInclusion,
/// PDF/X output intent profile name.
pub output_intent: String,
pub output_condition: String,
pub output_condition_id: String,
pub registry: String,
/// Mark the file as trapped.
pub trapped: bool,
}
/// The Advanced section.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AdvancedSettings {
/// Embed whole fonts when more than this share (%) of their characters is used.
pub font_subset_percent: f64,
/// Text as glyph outlines; off, text is real (selectable, searchable) text in embedded subset
/// fonts.
pub outline_text: bool,
pub overprint: Overprint,
}
impl Default for AdvancedSettings {
fn default() -> Self {
Self { font_subset_percent: 100.0, outline_text: true, overprint: Overprint::Preserve }
}
}
/// The Security section. Passwords are never serialized (presets and summaries leave them out).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct SecuritySettings {
#[serde(skip_serializing)]
pub open_password: String,
#[serde(skip_serializing)]
pub permissions_password: String,
pub printing: Printing,
pub changes: Changes,
/// Allow copying text, images and other content.
pub copy: bool,
/// Allow screen readers to read the text.
pub screen_reader: bool,
/// Leave the metadata unencrypted.
pub plaintext_metadata: bool,
}
impl Default for SecuritySettings {
fn default() -> Self {
Self {
open_password: String::new(),
permissions_password: String::new(),
printing: Printing::High,
changes: Changes::Any,
copy: true,
screen_reader: true,
plaintext_metadata: true,
}
}
}
/// `v` lies in `lo..=hi` (and is a number).
pub(crate) fn within(name: &str, v: f64, lo: f64, hi: f64, unit: &str) -> Result<(), PdfError> {
if (lo..=hi).contains(&v) { Ok(()) } else { Err(PdfError::BadSetting(format!("{name} must be {lo}–{hi}{unit} (got {v})"))) }
}
impl PdfSettings {
/// Refuse settings the writer can't honour: what [`Self::check_values`] refuses, a destination
/// profile that isn't there, or a PDF/X output intent it can't write ([`crate::pdfx`]).
pub fn check(&self) -> Result<(), PdfError> {
crate::output::check(self)?;
crate::pdfx::check(self)?;
self.check_values()
}
/// The part of [`Self::check`] a preset must pass (without the colour settings' profiles): a
/// standard with a PDF version it doesn't allow, a standard with editing data, PDF/X-1a or
/// PDF/X-3 with PDF layers, or an out-of-range value.
pub fn check_values(&self) -> Result<(), PdfError> {
self.security.check(self.standard, self.compatibility)?;
if !self.standard.allows(self.compatibility) {
return Err(PdfError::BadSetting(format!("{} files can't be {}", self.standard.label(), self.compatibility.label())));
}
if self.preserve_editing && self.standard != Standard::None {
return Err(PdfError::BadSetting(format!("{} files can't carry editing data: turn off preserveEditing", self.standard.label())));
}
if self.create_layers && !self.standard.allows_layers() {
return Err(PdfError::BadSetting(format!("{} files can't have PDF layers: turn off createLayers", self.standard.label())));
}
let c = &self.compression;
for (name, img) in
[("color", (c.color.ppi, c.color.above_ppi)), ("gray", (c.gray.ppi, c.gray.above_ppi)), ("mono", (c.mono.ppi, c.mono.above_ppi))]
{
within(&format!("compression.{name}.ppi"), img.0, 9.0, 2400.0, " ppi")?;
within(&format!("compression.{name}.abovePpi"), img.1, 9.0, 2400.0, " ppi")?;
}
self.marks.check()?;
self.bleed.check()?;
within("advanced.fontSubsetPercent", self.advanced.font_subset_percent, 0.0, 100.0, "%")
}
/// Forget the passwords (presets never store them).
pub fn clear_passwords(&mut self) {
self.security.open_password.clear();
self.security.permissions_password.clear();
}
/// Options that are accepted but not applied by the writer yet, one warning each.
pub fn warnings(&self) -> Vec<String> {
let d = Self::default();
let s = &self.security;
[
(self.thumbnails, "page thumbnails are not embedded yet"),
(self.fast_web_view, "fast web view (a linearised file) is not written yet"),
(self.create_layers, "PDF layers are not written yet: every layer is plain page content"),
(
!self.advanced.outline_text && self.advanced.font_subset_percent < 100.0,
"fonts are embedded as subsets of the characters used: a subset threshold below 100% is not applied",
),
(
!s.protected()
&& (s.printing != d.security.printing
|| s.changes != d.security.changes
|| s.copy != d.security.copy
|| s.screen_reader != d.security.screen_reader
|| s.plaintext_metadata != d.security.plaintext_metadata),
"permissions need a password: without one they are not applied",
),
]
.into_iter()
.filter(|(on, _)| *on)
.map(|(_, w)| w.to_string())
.chain(crate::encrypt::warnings(self))
.chain(crate::output::warnings(self))
.collect()
}
}
impl MarkSettings {
/// Refuse a weight or offset out of range (PDF export and print).
pub(crate) fn check(&self) -> Result<(), PdfError> {
within("marks.weight", self.weight, 0.05, 2.0, " pt")?;
within("marks.offset", self.offset, 0.0, 72.0, " pt")
}
}
impl BleedSettings {
/// Refuse a side out of range (PDF export and print).
pub(crate) fn check(&self) -> Result<(), PdfError> {
for (side, v) in ["top", "bottom", "left", "right"].iter().zip(self.values()) {
within(&format!("bleed.{side}"), v, 0.0, 72.0, " pt")?;
}
Ok(())
}
}