Files
vectorcraft/crates/pdf/src/presets.rs
T
bflatasticandClaude Opus 5.5 79581c7ff5 M4.87: PDF 1.3 compatibility: transparency flattened with the chosen preset
compatibility "1.3" writes a %PDF-1.3 file (its XMP metadata says 1.3
too) without transparency: a copy of the document is flattened first
with flattenerPreset (stored with PDF presets; empty is High Resolution)
and the flattener options over it, see-through image pixels counting as
transparency and rasterized areas clipped to their regions so no soft
mask is needed. Page groups are left out and the writer refuses a file
that would still have transparency. PDF/X-1a and PDF/X-3 share this
path (their version is now PDF 1.3; 1.4 is still accepted and written
as 1.3), and with it the preset, the header and metadata code and the
transparency check. Passwords at PDF 1.3 encrypt with 40-bit RC4
(revision 2). The Save PDF dialog lists PDF 1.3 and picks the flattener
preset under Advanced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 18:05:36 +07:00

104 lines
4.6 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.
//! PDF presets: named [`PdfSettings`]. The built-in ones are generated here (the app default,
//! which keeps the document editable, plus presets for print, press, small files and the PDF/X
//! standards); the user's own live in the app's preferences.
use serde::{Deserialize, Serialize};
use crate::{
BleedSettings, Compatibility, CompressionSettings, Downsample, ImageCodec, ImageCompression, JpegQuality, MonoCodec, MonoCompression,
PdfSettings, Standard,
};
/// The built-in preset every PDF export starts from.
pub const DEFAULT_PRESET: &str = "VectorCraft Default";
/// A named set of PDF settings: a built-in preset or one the user saved.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PdfPreset {
pub name: String,
/// What the preset is for.
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default)]
pub settings: PdfSettings,
}
/// Colour and greyscale images resampled (bicubic) to `ppi` above `above` ppi, compressed with
/// `codec`.
fn images(ppi: f64, above: f64, codec: ImageCodec, quality: JpegQuality) -> ImageCompression {
ImageCompression { downsample: Downsample::Bicubic, ppi, above_ppi: above, compression: codec, quality }
}
/// Compression with the same settings for colour and greyscale images, and CCITT Group 4
/// monochrome images resampled to `mono_ppi` above 1.5 × that.
fn compression(img: ImageCompression, mono_ppi: f64) -> CompressionSettings {
let mono = MonoCompression { downsample: Downsample::Bicubic, ppi: mono_ppi, above_ppi: mono_ppi * 1.5, compression: MonoCodec::CcittG4 };
CompressionSettings { color: img.clone(), gray: img, mono, compress_text: true }
}
/// Settings for print: PDF 1.6, images kept at 300 ppi, the document's bleed when `bleed`.
fn print(codec: ImageCodec, bleed: bool) -> PdfSettings {
PdfSettings {
compatibility: Compatibility::Pdf16,
compression: compression(images(300.0, 450.0, codec, JpegQuality::Maximum), 1200.0),
bleed: BleedSettings { use_document: bleed, ..Default::default() },
..Default::default()
}
}
/// A PDF/X preset: print settings for `standard` at PDF version `compatibility`.
fn pdf_x(standard: Standard, compatibility: Compatibility) -> PdfSettings {
PdfSettings { standard, compatibility, ..print(ImageCodec::Auto, true) }
}
/// Every built-in preset, the app default first.
pub fn builtin_presets() -> Vec<PdfPreset> {
let preset = |name: &str, description: &str, settings| PdfPreset { name: name.into(), description: description.into(), settings };
vec![
preset(
DEFAULT_PRESET,
"Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again.",
PdfSettings { preserve_editing: true, ..Default::default() },
),
preset("High Quality Print", "For desktop printers and proofs: images stay at print resolution.", print(ImageCodec::Auto, false)),
preset(
"Press Quality",
"For commercial printing: high-resolution images compressed losslessly, the document's bleed.",
print(ImageCodec::Zip, true),
),
preset(
"Smallest File Size",
"For screens, email and the web: images resampled to 100 ppi and compressed.",
PdfSettings {
compatibility: Compatibility::Pdf16,
fast_web_view: true,
compression: compression(images(100.0, 150.0, ImageCodec::Jpeg, JpegQuality::Medium), 300.0),
..Default::default()
},
),
preset(
"PDF/X-1a:2001",
"Print exchange with CMYK and spot colours only: transparency flattened, fonts embedded.",
pdf_x(Standard::PdfX1a, Compatibility::Pdf13),
),
preset(
"PDF/X-3:2002",
"Print exchange allowing colour-managed colours: transparency flattened, fonts embedded.",
pdf_x(Standard::PdfX3, Compatibility::Pdf13),
),
preset(
"PDF/X-4:2010",
"Print exchange keeping live transparency and layers, colour-managed colours and embedded fonts.",
pdf_x(Standard::PdfX4, Compatibility::Pdf16),
),
]
}
/// The built-in preset `name` names (any case; `default` is the app default).
pub fn builtin_preset(name: &str) -> Option<PdfPreset> {
let name = name.trim();
let name = if name.eq_ignore_ascii_case("default") { DEFAULT_PRESET } else { name };
builtin_presets().into_iter().find(|p| p.name.eq_ignore_ascii_case(name))
}