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>
This commit is contained in:
parent
8961824876
commit
79581c7ff5
|
|
@ -172,7 +172,7 @@ milestones, and honest time-to-parity estimates.
|
||||||
**Where we are (2026-10-05):** roughly 68–74% of Illustrator's features exist and work, and about 40–55% of
|
**Where we are (2026-10-05):** roughly 68–74% of Illustrator's features exist and work, and about 40–55% of
|
||||||
"a power user can't tell the difference". Everyday vector illustration is close to usable: drawing and path tools,
|
"a power user can't tell the difference". Everyday vector illustration is close to usable: drawing and path tools,
|
||||||
Pathfinder and Shape Builder, paint, gradients, appearance and transparency, type with styles and threading, and
|
Pathfinder and Shape Builder, paint, gradients, appearance and transparency, type with styles and threading, and
|
||||||
files (SVG, PDF and PDF-compatible `.ai`, EPS, DXF, raster formats, Print, Package). The scores are
|
files (SVG, PDF and PDF-compatible `.ai` with PDF/X, EPS, DXF, EMF/WMF, raster formats and PSD, Print, Package). The scores are
|
||||||
self-assessed, so the [honest assessment](ROADMAP.md#honest-assessment-2026-10-05) explains how far to trust them.
|
self-assessed, so the [honest assessment](ROADMAP.md#honest-assessment-2026-10-05) explains how far to trust them.
|
||||||
|
|
||||||
**What's missing:**
|
**What's missing:**
|
||||||
|
|
@ -187,7 +187,7 @@ self-assessed, so the [honest assessment](ROADMAP.md#honest-assessment-2026-10-0
|
||||||
advanced type, then hardening and packaging for 1.0. The prioritized list is in
|
advanced type, then hardening and packaging for 1.0. The prioritized list is in
|
||||||
[Where we're lacking](ROADMAP.md#where-were-lacking-in-priority-order).
|
[Where we're lacking](ROADMAP.md#where-were-lacking-in-priority-order).
|
||||||
|
|
||||||
**Workspace:** `crates/{geom, color, doc, pathops, text, effects, trace, brush, render, svg, pdf, format, tools, engine, ui-egui, mcp, testkit}`
|
**Workspace:** `crates/{geom, color, doc, pathops, text, effects, trace, brush, render, svg, pdf, eps, cad, metafile, format, tools, engine, ui-egui, mcp, testkit}`
|
||||||
and `apps/{vectorcraft, vectorcraft-cli, vectorcraft-web}`. The egui frontend is its own crate, so
|
and `apps/{vectorcraft, vectorcraft-cli, vectorcraft-web}`. The egui frontend is its own crate, so
|
||||||
the UI can be swapped without touching the engine.
|
the UI can be swapped without touching the engine.
|
||||||
|
|
||||||
|
|
|
||||||
10
ROADMAP.md
10
ROADMAP.md
|
|
@ -126,7 +126,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||||
- **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize.
|
- **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize.
|
||||||
- **Files (M4):**
|
- **Files (M4):**
|
||||||
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export.
|
- `.vectorcraft` (lossless JSON, compressed, atomic saves, save down to v1), Save As with a format chooser, Save a Copy, Revert, templates, Data Recovery after a crash, background save and export.
|
||||||
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view).
|
- SVG/SVGZ in and out with SVG Options, Preserve Editing, symbols, filters, rich text and physical units; PDF and PDF-compatible `.ai` in and out (layers, masks, editable text, spot colours, presets, security, marks and bleed, ICC output intents, raster effects, subset fonts, PDF/X, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3).
|
||||||
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export.
|
- EPS and DXF in and out, EMF/WMF in and out; PNG (with PNG-8), JPEG, WebP, GIF, TIFF, BMP, Targa and layered PSD export.
|
||||||
- Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours.
|
- Place and the Links panel, Package, File Info, Print with print presets, PostScript output and print tiling, slices and Save for Web, Export for Screens and Asset Export, CSS Properties, PNG/PDF/SVG/text clipboard flavours.
|
||||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped.
|
||||||
|
|
@ -139,8 +139,8 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||||
| M0 | Skeleton + vertical slice | ✅ done | — |
|
| M0 | Skeleton + vertical slice | ✅ done | — |
|
||||||
| M1 | Selection, transform, layers, MCP | ✅ mostly done (transform reference point snaps to anchors/centres; rotated persistent bbox pending) | 4–6 |
|
| M1 | Selection, transform, layers, MCP | ✅ mostly done (transform reference point snaps to anchors/centres; rotated persistent bbox pending) | 4–6 |
|
||||||
| M2 | Drawing tools + smart guides | ✅ mostly done (Flare, Reshape, Live Corners widget dragging landed; Shaper, Pen modifier nuances) | 15–20 |
|
| M2 | Drawing tools + smart guides | ✅ mostly done (Flare, Reshape, Live Corners widget dragging landed; Shaper, Pen modifier nuances) | 15–20 |
|
||||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork; gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool, Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries. Left: freeform and mixed spot/process gradients export as stops or process colours to SVG/PDF; flattener presets not yet used by PDF/print; confirm the Scale Strokes & Effects default | 4–8 |
|
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork; gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool, Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries. Left: freeform and mixed spot/process gradients export as stops or process colours to SVG/PDF; confirm the Scale Strokes & Effects default | 4–8 |
|
||||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ mostly done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view), Place and Links, Package, EPS and DXF in and out, EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info. Left: DWG (use DXF), PSD placement as layers, polish | 6–10 |
|
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out, EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, Asset Export, clipboard flavours, File Info. Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, polish | 3–6 |
|
||||||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder edge erase, large-offset bug open) | 3–6 |
|
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder edge erase, large-offset bug open) | 3–6 |
|
||||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel done; tab leaders, spell check, vertical type pending | 45–60 |
|
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs, OpenType panel, Find Font, Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects, tab stops + Tabs panel done; tab leaders, spell check, vertical type pending | 45–60 |
|
||||||
|
|
@ -174,7 +174,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||||
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 85% | Live Paint gap options, Shape Builder edge cases | 3–6 |
|
| Path operations, Pathfinder, Shape Builder, Live Paint | 5 | 85% | Live Paint gap options, Shape Builder edge cases | 3–6 |
|
||||||
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 96% | freeform and mixed spot/process gradients in SVG/PDF (exported as stops or process colours), pattern fills in Expand | 2–4 |
|
| Colour, swatches, gradients, patterns, mesh, recolor | 7 | 96% | freeform and mixed spot/process gradients in SVG/PDF (exported as stops or process colours), pattern fills in Expand | 2–4 |
|
||||||
| Strokes, brushes, width profiles | 5 | 88% | brush options depth, brush libraries (generated in code) | 6–10 |
|
| Strokes, brushes, width profiles | 5 | 88% | brush options depth, brush libraries (generated in code) | 6–10 |
|
||||||
| Appearance, transparency, graphic styles, masks | 5 | 97% | flattener presets in PDF/EPS/print, raster effect reach in preview bounds | 2–4 |
|
| Appearance, transparency, graphic styles, masks | 5 | 98% | raster effect reach in preview bounds (flattener presets apply in PDF, EPS and print) | 1–2 |
|
||||||
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters | 6–10 |
|
| Live vector effects | 5 | 85% | Outline Object, Pathfinder Hard/Soft Mix and Trap, SVG Filters | 6–10 |
|
||||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 20% | ~55 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video) and the Effect Gallery; Document Raster Effects Settings and raster effects in PDF are done | 28–42 |
|
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 20% | ~55 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video) and the Effect Gallery; Document Raster Effects Settings and raster effects in PDF are done | 28–42 |
|
||||||
| 3D and Materials | 4 | 0% | Extrude & Bevel, Revolve, Inflate, Rotate, lighting, materials (software renderer) | 50–80 |
|
| 3D and Materials | 4 | 0% | Extrude & Bevel, Revolve, Inflate, Rotate, lighting, materials (software renderer) | 50–80 |
|
||||||
|
|
@ -187,7 +187,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||||
| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth | 4–8 |
|
| Guides, grids, smart guides, snapping, rulers | 3 | 75% | global/video rulers, smart-guide preference depth | 4–8 |
|
||||||
| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers and presets are done | 6–10 |
|
| File formats | 6 | 85% | DWG (no open spec: DXF instead), PSD placement as layers, the remaining fidelity polish; EPS, DXF, EMF/WMF, TIFF, BMP, Targa, PSD export, SVGZ, PDF security, PDF/X, PDF layers and presets are done | 6–10 |
|
||||||
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
| Export for Screens, Asset Export, slices, Save for Web | 3 | 85% | polish only (all four are done) | 2–4 |
|
||||||
| Print, colour management, separations, flattener | 3 | 80% | flattener presets in PDF export, print preview fidelity (overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–3 |
|
| Print, colour management, separations, flattener | 3 | 80% | print preview fidelity (flattener presets in PDF and print, overprint in PDF and composite print, Print dialog, presets, PostScript, marks, separations, Print Tiling, print overprint/flattener/bitmap options and printer profile are done) | 1–2 |
|
||||||
| Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 |
|
| Automation | 3 | 60% | Variables (data merge), scripting surface, batch | 8–12 |
|
||||||
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | Variables, SVG Interactivity, Properties per context, Consolidate All Windows (Links, Asset Export, CSS Properties, Attributes are done) | 15–25 |
|
| UI chrome (panels, contextual Properties, workspaces, prefs) | 6 | 78% | Variables, SVG Interactivity, Properties per context, Consolidate All Windows (Links, Asset Export, CSS Properties, Attributes are done) | 15–25 |
|
||||||
| Libraries, Links, Package | 2 | 50% | a local Libraries panel (no cloud by design); Links and Package are done | 5–8 |
|
| Libraries, Links, Package | 2 | 50% | a local Libraries panel (no cloud by design); Links and Package are done | 5–8 |
|
||||||
|
|
|
||||||
|
|
@ -135,11 +135,7 @@ fn choice<T>(field: &str, v: Option<String>, read: impl Fn(&str) -> Option<T>, d
|
||||||
/// The flattener options `p` asks for (`flattenerPreset` and `flattener` over it), built-in
|
/// The flattener options `p` asks for (`flattenerPreset` and `flattener` over it), built-in
|
||||||
/// presets and `saved` ones; discarded overprints are dropped from the flattened art too.
|
/// presets and `saved` ones; discarded overprints are dropped from the flattened art too.
|
||||||
fn flattener(p: &Params, saved: &[super::super::FlattenerPreset]) -> Result<FlattenOptions> {
|
fn flattener(p: &Params, saved: &[super::super::FlattenerPreset]) -> Result<FlattenOptions> {
|
||||||
let mut q = json!({ "options": p.flattener.clone().unwrap_or(Value::Null) });
|
FlattenOptions::for_export(p.flattener_preset.as_deref(), p.flattener.as_ref(), saved).map_err(|e| bad(C, format!("EPS flattener: {e}")))
|
||||||
if let Some(name) = &p.flattener_preset {
|
|
||||||
q["preset"] = json!(name);
|
|
||||||
}
|
|
||||||
FlattenOptions::from_params_with(&q, saved).map_err(|e| bad(C, format!("EPS flattener: {e}")))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn params(p: &Value) -> Result<Params> {
|
fn params(p: &Value) -> Result<Params> {
|
||||||
|
|
@ -176,7 +172,7 @@ fn parse(p: &Value) -> Result<Settings> {
|
||||||
/// `saved` it names written out, as the encoder needs it.
|
/// `saved` it names written out, as the encoder needs it.
|
||||||
pub fn resolve(p: &Value, saved: &[super::super::FlattenerPreset]) -> std::result::Result<Value, String> {
|
pub fn resolve(p: &Value, saved: &[super::super::FlattenerPreset]) -> std::result::Result<Value, String> {
|
||||||
let run = || -> Result<Value> {
|
let run = || -> Result<Value> {
|
||||||
let q = expanded(p, saved)?.unwrap_or_else(|| p.clone());
|
let q = expanded(C, p, saved)?.unwrap_or_else(|| p.clone());
|
||||||
parse(&q)?;
|
parse(&q)?;
|
||||||
Ok(q)
|
Ok(q)
|
||||||
};
|
};
|
||||||
|
|
@ -185,10 +181,10 @@ pub fn resolve(p: &Value, saved: &[super::super::FlattenerPreset]) -> std::resul
|
||||||
|
|
||||||
/// `p` with a saved flattener preset it names written out as `flattener` options, for the
|
/// `p` with a saved flattener preset it names written out as `flattener` options, for the
|
||||||
/// encoder, which knows only the built-in presets; `None` when `p` names none.
|
/// encoder, which knows only the built-in presets; `None` when `p` names none.
|
||||||
fn expanded(p: &Value, saved: &[super::super::FlattenerPreset]) -> Result<Option<Value>> {
|
fn expanded(cmd: &str, p: &Value, saved: &[super::super::FlattenerPreset]) -> Result<Option<Value>> {
|
||||||
match str_param(p, "flattenerPreset") {
|
match str_param(p, "flattenerPreset") {
|
||||||
Some(name) if FlattenOptions::preset(name).is_none() => {
|
Some(name) if !name.trim().is_empty() && FlattenOptions::preset(name).is_none() => {
|
||||||
let o = flattener(¶ms(p)?, saved)?;
|
let o = FlattenOptions::for_export(Some(name), p.get("flattener"), saved).map_err(|e| bad(cmd, format!("flattener: {e}")))?;
|
||||||
let mut q = p.clone();
|
let mut q = p.clone();
|
||||||
q["flattener"] = serde_json::to_value(o).map_err(|e| EngineError::Other(e.to_string()))?;
|
q["flattener"] = serde_json::to_value(o).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||||
if let Some(m) = q.as_object_mut() {
|
if let Some(m) = q.as_object_mut() {
|
||||||
|
|
@ -299,8 +295,8 @@ fn export_eps(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `p` with a saved flattener preset of the session's written out (see [`expanded`]).
|
/// `p` with a saved flattener preset of the session's written out (see [`expanded`]).
|
||||||
pub(super) fn with_flattener<'a>(s: &Session, p: Cow<'a, Value>) -> Result<Cow<'a, Value>> {
|
pub(super) fn with_flattener<'a>(s: &Session, cmd: &str, p: Cow<'a, Value>) -> Result<Cow<'a, Value>> {
|
||||||
Ok(match expanded(&p, &s.prefs.flattener_presets)? {
|
Ok(match expanded(cmd, &p, &s.prefs.flattener_presets)? {
|
||||||
Some(q) => Cow::Owned(q),
|
Some(q) => Cow::Owned(q),
|
||||||
None => p,
|
None => p,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,7 @@ pub(super) fn without_artboards(p: &Value) -> Value {
|
||||||
/// `p` with the saved presets it names (PDF, flattener) written out as options, for the encoders,
|
/// `p` with the saved presets it names (PDF, flattener) written out as options, for the encoders,
|
||||||
/// which know only the built-in ones.
|
/// which know only the built-in ones.
|
||||||
fn expand_presets<'a>(s: &Session, cmd: &str, p: &'a Value) -> Result<Cow<'a, Value>> {
|
fn expand_presets<'a>(s: &Session, cmd: &str, p: &'a Value) -> Result<Cow<'a, Value>> {
|
||||||
let pdf = super::pdf::expand_preset(s, cmd, p)?;
|
super::pdf::expand_preset(s, cmd, p)
|
||||||
super::eps::with_flattener(s, pdf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The document an export of `f` writes: the active one (see [`export_source`]), or for text
|
/// The document an export of `f` writes: the active one (see [`export_source`]), or for text
|
||||||
|
|
|
||||||
|
|
@ -914,3 +914,6 @@ mod tests_pdflayers;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_pdfwebview;
|
mod tests_pdfwebview;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests_pdf13;
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -60,7 +60,9 @@ fn export_pdf_writes_the_compatibility_header_and_the_range() {
|
||||||
fn bad_options_are_refused() {
|
fn bad_options_are_refused() {
|
||||||
let mut s = session(2);
|
let mut s = session(2);
|
||||||
for p in [
|
for p in [
|
||||||
json!({"compatibility": "1.3"}),
|
json!({"compatibility": "1.2"}),
|
||||||
|
json!({"compatibility": "1.3", "standard": "pdfA2b"}),
|
||||||
|
json!({"compatibility": "1.3", "flattenerPreset": "Nope"}),
|
||||||
json!({"compatibility": 1.5}),
|
json!({"compatibility": 1.5}),
|
||||||
json!({"range": "1-3"}),
|
json!({"range": "1-3"}),
|
||||||
json!({"range": "0"}),
|
json!({"range": "0"}),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,155 @@
|
||||||
|
//! `document.exportPdf {compatibility: "1.3", flattenerPreset?, flattener?}`: a PDF 1.3 file has
|
||||||
|
//! no transparency (no soft masks, blend modes, constant opacity or transparency groups): it is
|
||||||
|
//! flattened with the chosen preset, and reads back looking like the canvas.
|
||||||
|
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use vectorcraft_geom::Rect;
|
||||||
|
use vectorcraft_render::{Rendered, Renderer};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A document with overlapping transparency: a half-opaque rectangle, a Multiply ellipse, a
|
||||||
|
/// gradient fading out, a drop shadow and a PNG with see-through pixels, in an isolated page.
|
||||||
|
fn session() -> Session {
|
||||||
|
let mut s = Session::new();
|
||||||
|
let run = |s: &mut Session, id: &str, p: Value| s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"));
|
||||||
|
run(&mut s, "file.new", json!({"width": 200, "height": 160}));
|
||||||
|
run(&mut s, "shape.rectangle", json!({"x": 10, "y": 10, "width": 90, "height": 70}));
|
||||||
|
run(&mut s, "paint.setFill", json!({"color": "#2060e0"}));
|
||||||
|
run(&mut s, "shape.rectangle", json!({"x": 50, "y": 40, "width": 90, "height": 70}));
|
||||||
|
run(&mut s, "paint.setFill", json!({"color": "#f02010"}));
|
||||||
|
run(&mut s, "transparency.set", json!({"opacity": 50}));
|
||||||
|
run(&mut s, "shape.ellipse", json!({"x": 90, "y": 20, "width": 80, "height": 80}));
|
||||||
|
run(&mut s, "paint.setFill", json!({"color": "#20c040"}));
|
||||||
|
run(&mut s, "transparency.set", json!({"blendMode": "multiply"}));
|
||||||
|
let shadowed = run(&mut s, "shape.rectangle", json!({"x": 20, "y": 100, "width": 50, "height": 40}))["id"].clone();
|
||||||
|
run(&mut s, "paint.setFill", json!({"color": "#e0c020"}));
|
||||||
|
run(&mut s, "effect.apply", json!({"effect": "stylize.dropShadow", "ids": [shadowed]}));
|
||||||
|
let mut png = vec![];
|
||||||
|
image::RgbaImage::from_fn(8, 8, |x, _| image::Rgba([0, 120, 255, if x < 4 { 0 } else { 255 }]))
|
||||||
|
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
run(&mut s, "file.place", json!({"name": "dots.png", "dataBase64": vectorcraft_format::base64_encode(&png), "at": [150, 120]}));
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn export(s: &mut Session, p: Value) -> (Vec<u8>, Vec<String>) {
|
||||||
|
let v = s.execute("document.exportPdf", &p).unwrap_or_else(|e| panic!("{p}: {e}"));
|
||||||
|
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
|
||||||
|
(bytes, v["warnings"].as_array().unwrap().iter().map(|w| w.as_str().unwrap().to_string()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The transparency `pdf` has, in its dictionaries.
|
||||||
|
fn transparency(pdf: &[u8]) -> Vec<&'static str> {
|
||||||
|
let text = String::from_utf8_lossy(pdf);
|
||||||
|
let mut found = vec![];
|
||||||
|
for (what, there) in [
|
||||||
|
("soft masks", text.contains("/SMask") && !text.contains("/SMask/None")),
|
||||||
|
("blend modes", text.contains("/BM") && !text.contains("/BM/Normal")),
|
||||||
|
("transparency groups", text.contains("/S/Transparency")),
|
||||||
|
("opacity", text.contains("/ca 0.") || text.contains("/CA 0.")),
|
||||||
|
] {
|
||||||
|
if there {
|
||||||
|
found.push(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(d: &vectorcraft_doc::Document) -> Rendered {
|
||||||
|
Renderer::new().render_region(d, Rect::new(0.0, 0.0, 200.0, 160.0), 1.0, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mean difference of the colour channels of two renders, as a share of full scale.
|
||||||
|
fn mean_diff(a: &Rendered, b: &Rendered) -> f64 {
|
||||||
|
let sum: u64 = a.pixels.iter().zip(&b.pixels).map(|(x, y)| x.abs_diff(*y) as u64).sum();
|
||||||
|
sum as f64 / a.pixels.len() as f64 / 255.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_1_3_flattens_all_transparency_and_looks_the_same() {
|
||||||
|
let mut s = session();
|
||||||
|
let before = s.doc().unwrap().doc.clone();
|
||||||
|
// The same document at PDF 1.4 has transparency.
|
||||||
|
let (pdf, _) = export(&mut s, json!({"compatibility": "1.4", "compression": {"compressText": false}, "preserveEditing": false}));
|
||||||
|
assert!(transparency(&pdf).len() >= 3, "{:?}", transparency(&pdf));
|
||||||
|
let (pdf, warnings) = export(&mut s, json!({"compatibility": "1.3", "compression": {"compressText": false}, "preserveEditing": false}));
|
||||||
|
assert!(pdf.starts_with(b"%PDF-1.3"));
|
||||||
|
assert!(String::from_utf8_lossy(&pdf).contains("<pdf:PDFVersion>1.3<"), "the metadata says 1.3 too");
|
||||||
|
assert_eq!(transparency(&pdf), Vec::<&str>::new());
|
||||||
|
assert!(warnings.iter().any(|w| w.contains("flattened") && w.contains("PDF 1.3")), "{warnings:?}");
|
||||||
|
// It reads back looking like the canvas.
|
||||||
|
let canvas = render(&before);
|
||||||
|
let diff = mean_diff(&canvas, &render(&vectorcraft_pdf::import(&pdf).unwrap()));
|
||||||
|
assert!(diff < 0.02, "read back {:.2}% off", diff * 100.0);
|
||||||
|
// The document itself is untouched.
|
||||||
|
assert_eq!(s.doc().unwrap().doc, before);
|
||||||
|
// The writer refuses a document that isn't flat; a flat one with isolated page blending gets
|
||||||
|
// no page group.
|
||||||
|
let opts = vectorcraft_pdf::PdfOptions { settings: serde_json::from_value(json!({"compatibility": "1.3"})).unwrap(), ..Default::default() };
|
||||||
|
let e = vectorcraft_pdf::export(&before, &opts).unwrap_err();
|
||||||
|
assert!(matches!(&e, vectorcraft_pdf::PdfError::Unsupported(m) if m.contains("PDF 1.3")), "{e}");
|
||||||
|
let mut flat = vectorcraft_doc::Document::new(100.0, 100.0);
|
||||||
|
flat.page_isolate = true;
|
||||||
|
let l = flat.default_layer().unwrap();
|
||||||
|
let id = flat.alloc_id();
|
||||||
|
let rect = vectorcraft_geom::shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 50.0));
|
||||||
|
let paint = vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK);
|
||||||
|
flat.insert(Some(l), 0, vectorcraft_doc::Node::path(id, rect, vectorcraft_doc::Appearance::basic(paint, vectorcraft_color::Paint::None, 0.0)))
|
||||||
|
.unwrap();
|
||||||
|
assert!(transparency(&vectorcraft_pdf::export(&flat, &opts).unwrap()).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_flattener_preset_and_options_apply() {
|
||||||
|
let mut s = session();
|
||||||
|
let images = |pdf: &[u8]| String::from_utf8_lossy(pdf).matches("/Subtype/Image").count();
|
||||||
|
let p = |extra: Value| {
|
||||||
|
let mut p = json!({"compatibility": "1.3", "preserveEditing": false});
|
||||||
|
super::pdf::merge(&mut p, &extra);
|
||||||
|
p
|
||||||
|
};
|
||||||
|
let (high, _) = export(&mut s, p(json!({})));
|
||||||
|
let (default, _) = export(&mut s, p(json!({"flattenerPreset": "High Resolution"})));
|
||||||
|
assert_eq!(high.len(), default.len(), "the default is High Resolution");
|
||||||
|
let (low, _) = export(&mut s, p(json!({"flattenerPreset": "low"})));
|
||||||
|
assert!(low.len() < high.len(), "lower resolution rasters");
|
||||||
|
// All raster: one image a flattened group at most, and still flat.
|
||||||
|
let (raster, _) = export(&mut s, p(json!({"flattener": {"balance": 0}})));
|
||||||
|
assert!(images(&raster) >= 1 && transparency(&raster).is_empty());
|
||||||
|
// A saved preset by name, through every PDF path; an unknown one is refused.
|
||||||
|
s.execute("flattener.presets.save", &json!({"name": "All Raster", "preset": "high", "balance": 0})).unwrap();
|
||||||
|
let (saved, _) = export(&mut s, p(json!({"flattenerPreset": "All Raster"})));
|
||||||
|
assert_eq!(saved.len(), raster.len());
|
||||||
|
let v = s
|
||||||
|
.execute("document.export", &json!({"format": "pdf", "compatibility": "1.3", "flattenerPreset": "All Raster", "preserveEditing": false}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap().len(), raster.len());
|
||||||
|
s.execute("pdf.preset.save", &json!({"name": "Old Press", "compatibility": "1.3", "flattenerPreset": "All Raster", "preserveEditing": false}))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(export(&mut s, json!({"preset": "Old Press"})).0.len(), raster.len(), "a PDF preset keeps its flattener preset");
|
||||||
|
assert!(s.execute("document.exportPdf", &p(json!({"flattenerPreset": "Nope"}))).is_err());
|
||||||
|
// Other versions keep their transparency, whatever the preset.
|
||||||
|
let (kept, warnings) = export(&mut s, json!({"flattenerPreset": "low", "compression": {"compressText": false}}));
|
||||||
|
assert!(!transparency(&kept).is_empty() && !warnings.iter().any(|w| w.contains("flattened")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_1_3_files_are_encrypted_with_40_bit_rc4_and_can_be_linearised() {
|
||||||
|
let mut s = session();
|
||||||
|
let (pdf, warnings) = export(
|
||||||
|
&mut s,
|
||||||
|
json!({"compatibility": "1.3", "fastWebView": true, "thumbnails": true, "security": {"openPassword": "open", "permissionsPassword": "owner", "plaintextMetadata": false}}),
|
||||||
|
);
|
||||||
|
assert!(pdf.starts_with(b"%PDF-1.3") && warnings.iter().all(|w| !w.contains("metadata")), "{warnings:?}");
|
||||||
|
let text = String::from_utf8_lossy(&pdf);
|
||||||
|
assert!(text.contains("/V 1/R 2/Length 40") && text.contains("/Linearized 1") && text.contains("/Thumb "));
|
||||||
|
for password in ["open", "owner"] {
|
||||||
|
s.execute("document.close", &json!({"force": true})).ok();
|
||||||
|
s.execute("document.open", &json!({"dataBase64": vectorcraft_format::base64_encode(&pdf), "name": "old.pdf", "password": password})).unwrap();
|
||||||
|
assert_eq!(s.doc().unwrap().doc.artboards.len(), 1, "{password}");
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
s.execute("document.open", &json!({"dataBase64": vectorcraft_format::base64_encode(&pdf), "name": "old.pdf", "password": "nope"})).is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,7 @@ fn choosing_a_pdfx_standard_sets_its_version_and_turns_off_what_it_forbids() {
|
||||||
let v = settings(&mut s, json!({"standard": "pdfX4"}));
|
let v = settings(&mut s, json!({"standard": "pdfX4"}));
|
||||||
assert_eq!((&v["compatibility"], &v["preserveEditing"]), (&json!("1.6"), &json!(false)));
|
assert_eq!((&v["compatibility"], &v["preserveEditing"]), (&json!("1.6"), &json!(false)));
|
||||||
let v = settings(&mut s, json!({"standard": "pdfX1a", "createLayers": null, "preset": "Press Quality"}));
|
let v = settings(&mut s, json!({"standard": "pdfX1a", "createLayers": null, "preset": "Press Quality"}));
|
||||||
assert_eq!((&v["compatibility"], &v["createLayers"]), (&json!("1.4"), &json!(false)));
|
assert_eq!((&v["compatibility"], &v["createLayers"]), (&json!("1.3"), &json!(false)));
|
||||||
let v = settings(&mut s, json!({"standard": "pdfX4", "compatibility": "1.5", "createLayers": true}));
|
let v = settings(&mut s, json!({"standard": "pdfX4", "compatibility": "1.5", "createLayers": true}));
|
||||||
assert_eq!((&v["compatibility"], &v["createLayers"]), (&json!("1.5"), &json!(true)));
|
assert_eq!((&v["compatibility"], &v["createLayers"]), (&json!("1.5"), &json!(true)));
|
||||||
// What is asked for explicitly is checked, not changed.
|
// What is asked for explicitly is checked, not changed.
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,16 @@ impl FlattenOptions {
|
||||||
Self::from_params_with(p, &[])
|
Self::from_params_with(p, &[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The options an export flattens with: preset `name` (built-in or one of `saved`; default
|
||||||
|
/// medium) with the export's `flattener` `options` over it.
|
||||||
|
pub fn for_export(name: Option<&str>, options: Option<&Value>, saved: &[FlattenerPreset]) -> std::result::Result<Self, String> {
|
||||||
|
let mut q = json!({ "options": options.cloned().unwrap_or(Value::Null) });
|
||||||
|
if let Some(name) = name {
|
||||||
|
q["preset"] = json!(name);
|
||||||
|
}
|
||||||
|
Self::from_params_with(&q, saved)
|
||||||
|
}
|
||||||
|
|
||||||
/// The options `p` asks for: `preset` (built-in or one of `saved`; default medium) adjusted by
|
/// The options `p` asks for: `preset` (built-in or one of `saved`; default medium) adjusted by
|
||||||
/// the option keys given at the top level or in `options`.
|
/// the option keys given at the top level or in `options`.
|
||||||
pub fn from_params_with(p: &Value, saved: &[FlattenerPreset]) -> std::result::Result<Self, String> {
|
pub fn from_params_with(p: &Value, saved: &[FlattenerPreset]) -> std::result::Result<Self, String> {
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
//!
|
//!
|
||||||
//! The export writes a plain file; [`protect`] then encrypts every string and stream in it (the
|
//! The export writes a plain file; [`protect`] then encrypts every string and stream in it (the
|
||||||
//! XMP metadata stays readable with Plaintext Metadata) and adds the encryption dictionary. The
|
//! XMP metadata stays readable with Plaintext Metadata) and adds the encryption dictionary. The
|
||||||
//! algorithm follows the compatibility ([`Encryption`]): 128-bit RC4 for PDF 1.4 (revision 3) and
|
//! algorithm follows the compatibility ([`Encryption`]): 40-bit RC4 for PDF 1.3 (revision 2),
|
||||||
//! 1.5 (revision 4), 128-bit AES for 1.6 (revision 4) and 256-bit AES for 1.7 and 2.0
|
//! 128-bit RC4 for PDF 1.4 (revision 3) and 1.5 (revision 4), 128-bit AES for 1.6 (revision 4) and
|
||||||
//! (revision 6). The random file key, salts and initialisation vectors come from a SHA-256 stream
|
//! 256-bit AES for 1.7 and 2.0 (revision 6). The random file key, salts and initialisation vectors
|
||||||
//! seeded with the process's random hash keys, the time and the file itself. A linearised file is
|
//! come from a SHA-256 stream seeded with the process's random hash keys, the time and the file
|
||||||
//! encrypted between its two steps ([`protect_keeping`]): what encrypted it then encrypts the hint
|
//! itself. A linearised file is encrypted between its two steps ([`protect_keeping`]): what
|
||||||
//! stream written last ([`Cipher`]).
|
//! encrypted it then encrypts the hint stream written last ([`Cipher`]).
|
||||||
//!
|
//!
|
||||||
//! Reading: [`user_password`] turns the permissions (owner) password of a file encrypted with RC4
|
//! Reading: [`user_password`] turns the permissions (owner) password of a file encrypted with RC4
|
||||||
//! or 128-bit AES into its open (user) password, so either password opens it.
|
//! or 128-bit AES into its open (user) password, so either password opens it.
|
||||||
|
|
@ -24,6 +24,7 @@ use crate::{Changes, Compatibility, PdfError, PdfSettings, Printing, SecuritySet
|
||||||
choice! {
|
choice! {
|
||||||
/// The encryption algorithm, which follows the PDF version (Compatibility).
|
/// The encryption algorithm, which follows the PDF version (Compatibility).
|
||||||
Encryption {
|
Encryption {
|
||||||
|
Rc440 = "rc440", "40-bit RC4";
|
||||||
Rc4 = "rc4", "128-bit RC4";
|
Rc4 = "rc4", "128-bit RC4";
|
||||||
Aes128 = "aes128", "128-bit AES";
|
Aes128 = "aes128", "128-bit AES";
|
||||||
Aes256 = "aes256", "256-bit AES";
|
Aes256 = "aes256", "256-bit AES";
|
||||||
|
|
@ -34,6 +35,7 @@ impl Encryption {
|
||||||
/// The algorithm a file of PDF version `c` is encrypted with.
|
/// The algorithm a file of PDF version `c` is encrypted with.
|
||||||
pub fn for_compatibility(c: Compatibility) -> Self {
|
pub fn for_compatibility(c: Compatibility) -> Self {
|
||||||
match c {
|
match c {
|
||||||
|
Compatibility::Pdf13 => Self::Rc440,
|
||||||
Compatibility::Pdf14 | Compatibility::Pdf15 => Self::Rc4,
|
Compatibility::Pdf14 | Compatibility::Pdf15 => Self::Rc4,
|
||||||
Compatibility::Pdf16 => Self::Aes128,
|
Compatibility::Pdf16 => Self::Aes128,
|
||||||
Compatibility::Pdf17 | Compatibility::Pdf20 => Self::Aes256,
|
Compatibility::Pdf17 | Compatibility::Pdf20 => Self::Aes256,
|
||||||
|
|
@ -45,6 +47,7 @@ impl Encryption {
|
||||||
/// encrypted), 4 (crypt filters: RC4 or AES-128) or 6 (AES-256).
|
/// encrypted), 4 (crypt filters: RC4 or AES-128) or 6 (AES-256).
|
||||||
fn revision(c: Compatibility) -> u8 {
|
fn revision(c: Compatibility) -> u8 {
|
||||||
match c {
|
match c {
|
||||||
|
Compatibility::Pdf13 => 2,
|
||||||
Compatibility::Pdf14 => 3,
|
Compatibility::Pdf14 => 3,
|
||||||
Compatibility::Pdf15 | Compatibility::Pdf16 => 4,
|
Compatibility::Pdf15 | Compatibility::Pdf16 => 4,
|
||||||
Compatibility::Pdf17 | Compatibility::Pdf20 => 6,
|
Compatibility::Pdf17 | Compatibility::Pdf20 => 6,
|
||||||
|
|
@ -140,7 +143,7 @@ impl PdfSettings {
|
||||||
pub(crate) fn warnings(set: &PdfSettings) -> Vec<String> {
|
pub(crate) fn warnings(set: &PdfSettings) -> Vec<String> {
|
||||||
let s = &set.security;
|
let s = &set.security;
|
||||||
let mut out = vec![];
|
let mut out = vec![];
|
||||||
if s.protected() && s.plaintext_metadata && revision(set.compatibility) == 3 {
|
if s.protected() && s.plaintext_metadata && revision(set.compatibility) <= 3 {
|
||||||
out.push(format!("{} encryption covers the metadata too: plaintext metadata needs PDF 1.5 or later", set.compatibility.label()));
|
out.push(format!("{} encryption covers the metadata too: plaintext metadata needs PDF 1.5 or later", set.compatibility.label()));
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
|
|
@ -337,7 +340,7 @@ impl Handler {
|
||||||
// Without a permissions password the open password grants every permission anyway.
|
// Without a permissions password the open password grants every permission anyway.
|
||||||
let owner = if sec.permissions_password.is_empty() { user } else { sec.permissions_password.as_bytes() };
|
let owner = if sec.permissions_password.is_empty() { user } else { sec.permissions_password.as_bytes() };
|
||||||
let p = sec.permission_bits();
|
let p = sec.permission_bits();
|
||||||
let encrypt_metadata = r == 3 || !sec.plaintext_metadata;
|
let encrypt_metadata = r <= 3 || !sec.plaintext_metadata;
|
||||||
let meta = if encrypt_metadata { "" } else { "/EncryptMetadata false" };
|
let meta = if encrypt_metadata { "" } else { "/EncryptMetadata false" };
|
||||||
if r == 6 {
|
if r == 6 {
|
||||||
let key: [u8; 32] = rng.bytes();
|
let key: [u8; 32] = rng.bytes();
|
||||||
|
|
@ -363,47 +366,59 @@ impl Handler {
|
||||||
);
|
);
|
||||||
return Ok(Self { key: key.to_vec(), cipher, encrypt_metadata, dict });
|
return Ok(Self { key: key.to_vec(), cipher, encrypt_metadata, dict });
|
||||||
}
|
}
|
||||||
// O (algorithm 3), the file key (algorithm 2) and U (algorithm 5), for a 16-byte key.
|
// O (algorithm 3), the file key (algorithm 2) and U (algorithms 4 and 5): a 5-byte key and
|
||||||
let owner_key = owner_key(owner, r, 16);
|
// single RC4 passes for revision 2, a 16-byte key for revisions 3 and 4.
|
||||||
|
let n = if r == 2 { 5 } else { 16 };
|
||||||
|
let owner_key = owner_key(owner, r, n);
|
||||||
let mut o = rc4(&owner_key, &padded(user));
|
let mut o = rc4(&owner_key, &padded(user));
|
||||||
for i in 1..=19 {
|
for i in (1..=19).filter(|_| r >= 3) {
|
||||||
o = rc4(&xored(&owner_key, i), &o);
|
o = rc4(&xored(&owner_key, i), &o);
|
||||||
}
|
}
|
||||||
let no_meta: &[u8] = if encrypt_metadata { &[] } else { &[0xFF; 4] };
|
let no_meta: &[u8] = if encrypt_metadata { &[] } else { &[0xFF; 4] };
|
||||||
let mut key = md5(&[&padded(user), &o, &p.to_le_bytes(), id, no_meta]);
|
let mut key = md5(&[&padded(user), &o, &p.to_le_bytes(), id, no_meta]);
|
||||||
for _ in 0..50 {
|
for _ in (0..50).filter(|_| r >= 3) {
|
||||||
key = md5(&[&key]);
|
key = md5(&[&key]);
|
||||||
}
|
}
|
||||||
let mut u = rc4(&key, &md5(&[&PAD, id]));
|
let key = key.get(..n).unwrap_or(&key).to_vec();
|
||||||
for i in 1..=19 {
|
let u = if r == 2 {
|
||||||
u = rc4(&xored(&key, i), &u);
|
rc4(&key, &PAD)
|
||||||
}
|
|
||||||
u.extend_from_slice(&rng.bytes::<16>());
|
|
||||||
let dict = if r == 3 {
|
|
||||||
format!("<</Filter/Standard/V 2/R 3/Length 128/P {p}/O{}/U{}>>", hex(&o), hex(&u))
|
|
||||||
} else {
|
} else {
|
||||||
let cfm = if cipher == Encryption::Aes128 { "AESV2" } else { "V2" };
|
let mut u = rc4(&key, &md5(&[&PAD, id]));
|
||||||
format!(
|
for i in 1..=19 {
|
||||||
"<</Filter/Standard/V 4/R 4/Length 128/CF<</StdCF<</AuthEvent/DocOpen/CFM/{cfm}/Length 16>>>>/StmF/StdCF/StrF/StdCF/P {p}/O{}/U{}{meta}>>",
|
u = rc4(&xored(&key, i), &u);
|
||||||
hex(&o),
|
}
|
||||||
hex(&u)
|
u.extend_from_slice(&rng.bytes::<16>());
|
||||||
)
|
u
|
||||||
};
|
};
|
||||||
Ok(Self { key: key.to_vec(), cipher, encrypt_metadata, dict })
|
let dict = match r {
|
||||||
|
2 => format!("<</Filter/Standard/V 1/R 2/Length 40/P {p}/O{}/U{}>>", hex(&o), hex(&u)),
|
||||||
|
3 => format!("<</Filter/Standard/V 2/R 3/Length 128/P {p}/O{}/U{}>>", hex(&o), hex(&u)),
|
||||||
|
_ => {
|
||||||
|
let cfm = if cipher == Encryption::Aes128 { "AESV2" } else { "V2" };
|
||||||
|
format!(
|
||||||
|
"<</Filter/Standard/V 4/R 4/Length 128/CF<</StdCF<</AuthEvent/DocOpen/CFM/{cfm}/Length 16>>>>/StmF/StdCF/StrF/StdCF/P {p}/O{}/U{}{meta}>>",
|
||||||
|
hex(&o),
|
||||||
|
hex(&u)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Self { key, cipher, encrypt_metadata, dict })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `data` of object `num generation` encrypted.
|
/// `data` of object `num generation` encrypted.
|
||||||
fn encrypt(&self, num: u32, generation: u16, data: &[u8], rng: &mut Entropy) -> Result<Vec<u8>, PdfError> {
|
fn encrypt(&self, num: u32, generation: u16, data: &[u8], rng: &mut Entropy) -> Result<Vec<u8>, PdfError> {
|
||||||
|
// The object's key: the file key's length plus 5 bytes, at most 16 (algorithm 1).
|
||||||
let object_key = || {
|
let object_key = || {
|
||||||
let salt: &[u8] = if self.cipher == Encryption::Aes128 { b"sAlT" } else { &[] };
|
let salt: &[u8] = if self.cipher == Encryption::Aes128 { b"sAlT" } else { &[] };
|
||||||
md5(&[&self.key, &num.to_le_bytes()[..3], &generation.to_le_bytes(), salt])
|
let h = md5(&[&self.key, &num.to_le_bytes()[..3], &generation.to_le_bytes(), salt]);
|
||||||
|
h.get(..(self.key.len() + 5).min(16)).unwrap_or(&h).to_vec()
|
||||||
};
|
};
|
||||||
let aes = |key: &[u8], rng: &mut Entropy| -> Result<Vec<u8>, PdfError> {
|
let aes = |key: &[u8], rng: &mut Entropy| -> Result<Vec<u8>, PdfError> {
|
||||||
let iv: [u8; 16] = rng.bytes();
|
let iv: [u8; 16] = rng.bytes();
|
||||||
Ok([&iv[..], &Aes::new(key)?.cbc(iv, data, true)].concat())
|
Ok([&iv[..], &Aes::new(key)?.cbc(iv, data, true)].concat())
|
||||||
};
|
};
|
||||||
match self.cipher {
|
match self.cipher {
|
||||||
Encryption::Rc4 => Ok(rc4(&object_key(), data)),
|
Encryption::Rc440 | Encryption::Rc4 => Ok(rc4(&object_key(), data)),
|
||||||
Encryption::Aes128 => aes(&object_key(), rng),
|
Encryption::Aes128 => aes(&object_key(), rng),
|
||||||
Encryption::Aes256 => aes(&self.key, rng),
|
Encryption::Aes256 => aes(&self.key, rng),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,8 @@ pub(crate) struct Writer {
|
||||||
/// Spot colours written with a Lab alternate, gathered from the exporters ([`Self::absorb`]).
|
/// Spot colours written with a Lab alternate, gathered from the exporters ([`Self::absorb`]).
|
||||||
lab_spots: Vec<(String, vectorcraft_color::cms::Lab)>,
|
lab_spots: Vec<(String, vectorcraft_color::cms::Lab)>,
|
||||||
warnings: Vec<String>,
|
warnings: Vec<String>,
|
||||||
|
/// A PDF 1.3 file ([`crate::PdfSettings::pdf13`]): flat, without page groups.
|
||||||
|
pdf13: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One page as [`Writer::page`] draws it: the art placed in a drawing space ([`Self::place`]),
|
/// One page as [`Writer::page`] draws it: the art placed in a drawing space ([`Self::place`]),
|
||||||
|
|
@ -171,7 +173,8 @@ impl Writer {
|
||||||
warnings.push(format!("the CMYK profile {} can't be embedded: CMYK colours are written untagged", out.cmyk_profile()));
|
warnings.push(format!("the CMYK profile {} can't be embedded: CMYK colours are written untagged", out.cmyk_profile()));
|
||||||
}
|
}
|
||||||
let version = match set.compatibility {
|
let version = match set.compatibility {
|
||||||
Compatibility::Pdf14 => PdfVersion::Pdf14,
|
// PDF 1.3 files are written with the PDF 1.4 settings: flat, they use nothing newer.
|
||||||
|
Compatibility::Pdf13 | Compatibility::Pdf14 => PdfVersion::Pdf14,
|
||||||
Compatibility::Pdf15 => PdfVersion::Pdf15,
|
Compatibility::Pdf15 => PdfVersion::Pdf15,
|
||||||
Compatibility::Pdf16 => PdfVersion::Pdf16,
|
Compatibility::Pdf16 => PdfVersion::Pdf16,
|
||||||
Compatibility::Pdf17 => PdfVersion::Pdf17,
|
Compatibility::Pdf17 => PdfVersion::Pdf17,
|
||||||
|
|
@ -212,7 +215,7 @@ impl Writer {
|
||||||
meta = meta.creation_date(t);
|
meta = meta.creation_date(t);
|
||||||
}
|
}
|
||||||
pdf.set_metadata(meta);
|
pdf.set_metadata(meta);
|
||||||
Ok(Self { pdf, cmyk: out.blends_cmyk(cmyk), out: Arc::new(out), lab_spots: vec![], warnings })
|
Ok(Self { pdf, cmyk: out.blends_cmyk(cmyk), out: Arc::new(out), lab_spots: vec![], warnings, pdf13: set.pdf13() })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw one page: the art of `ex`'s document placed as `sheet` says, then its marks.
|
/// Draw one page: the art of `ex`'s document placed as `sheet` says, then its marks.
|
||||||
|
|
@ -241,10 +244,11 @@ impl Writer {
|
||||||
s.push_clip_path(clip, &krilla::paint::FillRule::NonZero);
|
s.push_clip_path(clip, &krilla::paint::FillRule::NonZero);
|
||||||
}
|
}
|
||||||
// Page Isolated Blending / Page Knockout Group: the page content is one group (the PDF
|
// Page Isolated Blending / Page Knockout Group: the page content is one group (the PDF
|
||||||
// writer has no page group attributes). In CMYK, transparency at the top of a page is put
|
// writer has no page group attributes; a flat PDF 1.3 file has none). In CMYK, transparency
|
||||||
// in a non-isolated group of its own, whose blending space `cmyk_blending` rewrites.
|
// at the top of a page is put in a non-isolated group of its own, whose blending space
|
||||||
|
// `cmyk_blending` rewrites.
|
||||||
let doc = ex.doc;
|
let doc = ex.doc;
|
||||||
let page_group = doc.page_isolate || doc.page_knockout;
|
let page_group = !self.pdf13 && (doc.page_isolate || doc.page_knockout);
|
||||||
let cmyk_page_group = self.cmyk && !page_group && ex.shows_transparency();
|
let cmyk_page_group = self.cmyk && !page_group && ex.shows_transparency();
|
||||||
if page_group {
|
if page_group {
|
||||||
s.push_isolated();
|
s.push_isolated();
|
||||||
|
|
@ -311,7 +315,7 @@ impl Writer {
|
||||||
let bytes = if lab { crate::lab_spot::lab_alternates(bytes, &self.lab_spots) } else { bytes };
|
let bytes = if lab { crate::lab_spot::lab_alternates(bytes, &self.lab_spots) } else { bytes };
|
||||||
let (bytes, more) = self.out.write_catalog(bytes)?;
|
let (bytes, more) = self.out.write_catalog(bytes)?;
|
||||||
self.warnings.extend(more);
|
self.warnings.extend(more);
|
||||||
Ok((crate::pdfx::finish(bytes, self.out.standard)?, self.warnings))
|
Ok((crate::pdfx::finish(bytes, self.out.standard, self.pdf13)?, self.warnings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
//! Advanced, Security); options the writer doesn't apply yet come back as warnings. With Preserve
|
//! Advanced, Security); options the writer doesn't apply yet come back as warnings. With Preserve
|
||||||
//! Editing the native document rides along as an embedded file ([`editing()`]). Each page can
|
//! Editing the native document rides along as an embedded file ([`editing()`]). Each page can
|
||||||
//! carry a thumbnail ([`Thumbnail`], drawn by the caller), and Fast Web View writes a linearised
|
//! carry a thumbnail ([`Thumbnail`], drawn by the caller), and Fast Web View writes a linearised
|
||||||
//! file (still linearised when encrypted).
|
//! file (still linearised when encrypted). A PDF 1.3 file ([`PdfSettings::pdf13`]) must be flat:
|
||||||
|
//! the app flattens the document first, and the writer refuses transparency that is left.
|
||||||
//! - [`import`] reads PDF (and PDF-compatible `.ai`) pages with `hayro-interpret` into a
|
//! - [`import`] reads PDF (and PDF-compatible `.ai`) pages with `hayro-interpret` into a
|
||||||
//! [`Document`]: one artboard per page, and a layer per page or per optional content group
|
//! [`Document`]: one artboard per page, and a layer per page or per optional content group
|
||||||
//! (with its visibility, print state and lock; art that is off comes in as a hidden layer),
|
//! (with its visibility, print state and lock; art that is off comes in as a hidden layer),
|
||||||
|
|
@ -235,6 +236,8 @@ mod tests_marks;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_output;
|
mod tests_output;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod tests_pdf13;
|
||||||
|
#[cfg(test)]
|
||||||
mod tests_pdfx;
|
mod tests_pdfx;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_presets;
|
mod tests_presets;
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
//! document information, a title and dates, embedded fonts (or text as outlines), no editing
|
//! document information, a title and dates, embedded fonts (or text as outlines), no editing
|
||||||
//! data and no encryption (both refused by the settings checks).
|
//! data and no encryption (both refused by the settings checks).
|
||||||
//!
|
//!
|
||||||
//! PDF 1.3 files are written with the PDF 1.4 settings (nothing above 1.3 is used once the art is
|
//! PDF/X-1a and PDF/X-3 files are PDF 1.3 files ([`crate::PdfSettings::pdf13`]): flat, written with
|
||||||
//! flat) and their header and metadata say 1.3. The written file is checked ([`verify`]): what the
|
//! the PDF 1.4 settings, their header and metadata saying 1.3. The written file is checked ([`verify`]): what the
|
||||||
//! standard forbids that is still there refuses the export, rather than a file claiming a
|
//! standard forbids that is still there refuses the export, rather than a file claiming a
|
||||||
//! standard it breaks.
|
//! standard it breaks.
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ pub(crate) fn info_entries(standard: Standard) -> String {
|
||||||
|
|
||||||
/// Edit the XMP metadata of a PDF/X file (the catalog's `/Metadata` stream; `root` is the
|
/// Edit the XMP metadata of a PDF/X file (the catalog's `/Metadata` stream; `root` is the
|
||||||
/// catalog dictionary's contents) into `patch`: PDF/X-4 adds the standard, Trapped and a version
|
/// catalog dictionary's contents) into `patch`: PDF/X-4 adds the standard, Trapped and a version
|
||||||
/// id; PDF 1.3 files say so. Files without metadata are left as they are.
|
/// id (PDF 1.3 files say so in [`finish`]). Files without metadata are left as they are.
|
||||||
pub(crate) fn xmp(pdf: &[u8], xref: &Xref, root: (usize, usize), patch: &mut Patch, standard: Standard, trapped: bool) -> Result<(), PdfError> {
|
pub(crate) fn xmp(pdf: &[u8], xref: &Xref, root: (usize, usize), patch: &mut Patch, standard: Standard, trapped: bool) -> Result<(), PdfError> {
|
||||||
if !standard.is_pdfx() {
|
if !standard.is_pdfx() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
@ -137,12 +137,6 @@ pub(crate) fn xmp(pdf: &[u8], xref: &Xref, root: (usize, usize), patch: &mut Pat
|
||||||
let data = data + obj.get(data..).map_or(0, |r| r.iter().take(2).take_while(|b| matches!(b, b'\r' | b'\n')).count());
|
let data = data + obj.get(data..).map_or(0, |r| r.iter().take(2).take_while(|b| matches!(b, b'\r' | b'\n')).count());
|
||||||
let xml = data.checked_add(length).and_then(|e| obj.get(data..e)).ok_or_else(bad)?;
|
let xml = data.checked_add(length).and_then(|e| obj.get(data..e)).ok_or_else(bad)?;
|
||||||
let mut edits: Vec<(usize, usize, Vec<u8>)> = vec![];
|
let mut edits: Vec<(usize, usize, Vec<u8>)> = vec![];
|
||||||
if standard.flattens()
|
|
||||||
&& let Some(v) = find(xml, b"<pdf:PDFVersion>1.4<", 0)
|
|
||||||
{
|
|
||||||
let digit = v + b"<pdf:PDFVersion>1.".len();
|
|
||||||
edits.push((digit, digit + 1, b"3".to_vec()));
|
|
||||||
}
|
|
||||||
if let Some((version, _)) = standard.pdfx_ids().filter(|_| standard == Standard::PdfX4) {
|
if let Some((version, _)) = standard.pdfx_ids().filter(|_| standard == Standard::PdfX4) {
|
||||||
// Into the writer's description, whose namespaces declare the `pdf` and `xmpMM` prefixes.
|
// Into the writer's description, whose namespaces declare the `pdf` and `xmpMM` prefixes.
|
||||||
let close = crate::lab_spot::rfind(xml, b"</rdf:Description>").ok_or_else(bad)?;
|
let close = crate::lab_spot::rfind(xml, b"</rdf:Description>").ok_or_else(bad)?;
|
||||||
|
|
@ -163,18 +157,30 @@ pub(crate) fn xmp(pdf: &[u8], xref: &Xref, root: (usize, usize), patch: &mut Pat
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The finished file of `standard`: its header version (PDF 1.3 for PDF/X-1a and PDF/X-3), then
|
/// The finished file of `standard`: a PDF 1.3 file (`pdf13`: PDF/X-1a, PDF/X-3, or asked for) says
|
||||||
/// checked ([`verify`]).
|
/// so ([`crate::post::declare_pdf13`]) and must be flat, then the file is checked against its
|
||||||
pub(crate) fn finish(mut pdf: Vec<u8>, standard: Standard) -> Result<Vec<u8>, PdfError> {
|
/// standard ([`verify`]).
|
||||||
if standard.flattens()
|
pub(crate) fn finish(mut pdf: Vec<u8>, standard: Standard, pdf13: bool) -> Result<Vec<u8>, PdfError> {
|
||||||
&& let Some(v) = pdf.get_mut(..8).filter(|h| h.starts_with(b"%PDF-1.4"))
|
if pdf13 {
|
||||||
{
|
crate::post::declare_pdf13(&mut pdf);
|
||||||
v.copy_from_slice(b"%PDF-1.3");
|
if !standard.flattens() {
|
||||||
|
flat(&without_streams(&pdf), "PDF 1.3")?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
verify(&pdf, standard)?;
|
verify(&pdf, standard)?;
|
||||||
Ok(pdf)
|
Ok(pdf)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Refuse a file (`objects`, without its stream data) with transparency: `kind` files have none.
|
||||||
|
fn flat(objects: &[u8], kind: &str) -> Result<(), PdfError> {
|
||||||
|
match transparency(objects) {
|
||||||
|
Some(what) => Err(PdfError::Unsupported(format!(
|
||||||
|
"{kind} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency)"
|
||||||
|
))),
|
||||||
|
None => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Refuse a written file that breaks `standard`: transparency in PDF/X-1a and PDF/X-3 (soft
|
/// Refuse a written file that breaks `standard`: transparency in PDF/X-1a and PDF/X-3 (soft
|
||||||
/// masks, transparency groups, constant opacity, blend modes), colour spaces other than CMYK,
|
/// masks, transparency groups, constant opacity, blend modes), colour spaces other than CMYK,
|
||||||
/// grey and spot inks in PDF/X-1a, and fonts that aren't embedded.
|
/// grey and spot inks in PDF/X-1a, and fonts that aren't embedded.
|
||||||
|
|
@ -184,12 +190,8 @@ pub(crate) fn verify(pdf: &[u8], standard: Standard) -> Result<(), PdfError> {
|
||||||
}
|
}
|
||||||
let objects = without_streams(pdf);
|
let objects = without_streams(pdf);
|
||||||
let std = standard.label();
|
let std = standard.label();
|
||||||
if standard.flattens()
|
if standard.flattens() {
|
||||||
&& let Some(what) = transparency(&objects)
|
flat(&objects, std)?;
|
||||||
{
|
|
||||||
return Err(PdfError::Unsupported(format!(
|
|
||||||
"{std} files have no transparency, and the file would have {what}: flatten it first (object.flattenTransparency)"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
if standard.cmyk_only()
|
if standard.cmyk_only()
|
||||||
&& let Some(space) =
|
&& let Some(space) =
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
//! What the export does to the file once it is written, in this order: the pages' thumbnails
|
//! What the export does to the file once it is written, in this order: the pages' thumbnails
|
||||||
//! (General › Embed page thumbnails), the seal of the editing data, fast web view (a linearised
|
//! (General › Embed page thumbnails), the seal of the editing data, fast web view (a linearised
|
||||||
//! file, [`crate::linearize`]) and encryption ([`crate::encrypt`]), which a linearised file goes
|
//! file, [`crate::linearize`]) and encryption ([`crate::encrypt`]), which a linearised file goes
|
||||||
//! through between its two steps so that it stays linearised.
|
//! through between its two steps so that it stays linearised. Also here: what makes a file say it
|
||||||
|
//! is PDF 1.3 ([`declare_pdf13`]), for PDF 1.3 and PDF/X-1a and PDF/X-3 files.
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::encrypt::Obj;
|
||||||
use crate::linearize::{self, Parsed};
|
use crate::linearize::{self, Parsed};
|
||||||
use crate::patch::Patch;
|
use crate::patch::Patch;
|
||||||
use crate::{PdfError, PdfOptions, PdfSettings};
|
use crate::{PdfError, PdfOptions, PdfSettings};
|
||||||
|
|
@ -89,3 +91,24 @@ pub(crate) fn finish(pdf: Vec<u8>, opts: &PdfOptions, editing: bool, warnings: &
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make a file written with the PDF 1.4 settings say it is PDF 1.3: its header and the version
|
||||||
|
/// its XMP metadata gives, rewritten in place.
|
||||||
|
pub(crate) fn declare_pdf13(pdf: &mut [u8]) {
|
||||||
|
if let Some(v) = pdf.get_mut(..8).filter(|h| h.starts_with(b"%PDF-1.4")) {
|
||||||
|
v.copy_from_slice(b"%PDF-1.3");
|
||||||
|
}
|
||||||
|
if let Some(digit) = metadata_version(pdf).and_then(|at| pdf.get_mut(at)) {
|
||||||
|
*digit = b'3';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the minor version digit is in the `1.4` of the XMP metadata stream of `pdf`'s catalog.
|
||||||
|
fn metadata_version(pdf: &[u8]) -> Option<usize> {
|
||||||
|
const VERSION: &[u8] = b"<pdf:PDFVersion>1.4<";
|
||||||
|
let p = linearize::parse(pdf).ok()?;
|
||||||
|
let Some(&Obj::Ref(n, _)) = p.objects.get(&p.root)?.value.get(b"Metadata") else { return None };
|
||||||
|
let o = p.objects.get(&n)?;
|
||||||
|
let at = crate::lab_spot::find(pdf.get(..o.endobj)?, VERSION, o.value_span.1)?;
|
||||||
|
Some(at + VERSION.len() - 2)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,12 +80,12 @@ pub fn builtin_presets() -> Vec<PdfPreset> {
|
||||||
preset(
|
preset(
|
||||||
"PDF/X-1a:2001",
|
"PDF/X-1a:2001",
|
||||||
"Print exchange with CMYK and spot colours only: transparency flattened, fonts embedded.",
|
"Print exchange with CMYK and spot colours only: transparency flattened, fonts embedded.",
|
||||||
pdf_x(Standard::PdfX1a, Compatibility::Pdf14),
|
pdf_x(Standard::PdfX1a, Compatibility::Pdf13),
|
||||||
),
|
),
|
||||||
preset(
|
preset(
|
||||||
"PDF/X-3:2002",
|
"PDF/X-3:2002",
|
||||||
"Print exchange allowing colour-managed colours: transparency flattened, fonts embedded.",
|
"Print exchange allowing colour-managed colours: transparency flattened, fonts embedded.",
|
||||||
pdf_x(Standard::PdfX3, Compatibility::Pdf14),
|
pdf_x(Standard::PdfX3, Compatibility::Pdf13),
|
||||||
),
|
),
|
||||||
preset(
|
preset(
|
||||||
"PDF/X-4:2010",
|
"PDF/X-4:2010",
|
||||||
|
|
|
||||||
|
|
@ -67,26 +67,25 @@ choice! {
|
||||||
PdfX4 = "pdfX4", "PDF/X-4:2010";
|
PdfX4 = "pdfX4", "PDF/X-4:2010";
|
||||||
} default None
|
} default None
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Standard {
|
impl Standard {
|
||||||
/// A file of this standard can declare PDF version `c`: PDF/A-2 is based on PDF 1.7 (so it
|
/// 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
|
/// can't be PDF 2.0, nor PDF 1.3, which has no transparency), PDF/X-4 on PDF 1.6, and PDF/X-1a
|
||||||
/// writer writes as 1.4 (the file says 1.3, see [`crate::pdfx`]).
|
/// and PDF/X-3 on PDF 1.3 (PDF 1.4 is accepted too, and written as 1.3).
|
||||||
pub fn allows(self, c: Compatibility) -> bool {
|
pub fn allows(self, c: Compatibility) -> bool {
|
||||||
use Compatibility::*;
|
use Compatibility::*;
|
||||||
match self {
|
match self {
|
||||||
Self::None => true,
|
Self::None => true,
|
||||||
Self::PdfA2b => c != Pdf20,
|
Self::PdfA2b => !matches!(c, Pdf13 | Pdf20),
|
||||||
Self::PdfX1a | Self::PdfX3 => c == Pdf14,
|
Self::PdfX1a | Self::PdfX3 => matches!(c, Pdf13 | Pdf14),
|
||||||
Self::PdfX4 => matches!(c, Pdf14 | Pdf15 | Pdf16),
|
Self::PdfX4 => matches!(c, Pdf14 | Pdf15 | Pdf16),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The latest PDF version a file of this standard can declare (what choosing it sets).
|
/// The PDF version a file of this standard is (what choosing it sets).
|
||||||
pub fn version(self) -> Compatibility {
|
pub fn version(self) -> Compatibility {
|
||||||
match self {
|
match self {
|
||||||
Self::None | Self::PdfA2b => Compatibility::Pdf17,
|
Self::None | Self::PdfA2b => Compatibility::Pdf17,
|
||||||
Self::PdfX1a | Self::PdfX3 => Compatibility::Pdf14,
|
Self::PdfX1a | Self::PdfX3 => Compatibility::Pdf13,
|
||||||
Self::PdfX4 => Compatibility::Pdf16,
|
Self::PdfX4 => Compatibility::Pdf16,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +94,8 @@ impl Standard {
|
||||||
choice! {
|
choice! {
|
||||||
/// The PDF version written in the file header.
|
/// The PDF version written in the file header.
|
||||||
Compatibility {
|
Compatibility {
|
||||||
|
/// No transparency: it is flattened (see [`PdfSettings::pdf13`]).
|
||||||
|
Pdf13 = "1.3", "PDF 1.3";
|
||||||
Pdf14 = "1.4", "PDF 1.4";
|
Pdf14 = "1.4", "PDF 1.4";
|
||||||
Pdf15 = "1.5", "PDF 1.5";
|
Pdf15 = "1.5", "PDF 1.5";
|
||||||
Pdf16 = "1.6", "PDF 1.6";
|
Pdf16 = "1.6", "PDF 1.6";
|
||||||
|
|
@ -236,6 +237,10 @@ pub struct PdfSettings {
|
||||||
pub output: OutputSettings,
|
pub output: OutputSettings,
|
||||||
pub advanced: AdvancedSettings,
|
pub advanced: AdvancedSettings,
|
||||||
pub security: SecuritySettings,
|
pub security: SecuritySettings,
|
||||||
|
/// The transparency flattener preset PDF 1.3 files (PDF/X-1a and PDF/X-3 ones too) are
|
||||||
|
/// flattened with: a built-in or saved preset's name; empty, High Resolution. The app flattens
|
||||||
|
/// the document before writing; the writer doesn't read it.
|
||||||
|
pub flattener_preset: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resampling and compression of one kind of image.
|
/// Resampling and compression of one kind of image.
|
||||||
|
|
@ -449,6 +454,13 @@ impl PdfSettings {
|
||||||
self.create_layers && self.compatibility.has_layers()
|
self.create_layers && self.compatibility.has_layers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The file is PDF 1.3 (asked for, or a PDF/X-1a or PDF/X-3 file): it has no transparency (the
|
||||||
|
/// app flattens it, see [`Self::flattener_preset`]), is written with the PDF 1.4 settings, and
|
||||||
|
/// its header and metadata say 1.3.
|
||||||
|
pub fn pdf13(&self) -> bool {
|
||||||
|
self.compatibility == Compatibility::Pdf13 || self.standard.flattens()
|
||||||
|
}
|
||||||
|
|
||||||
/// Forget the passwords (presets never store them).
|
/// Forget the passwords (presets never store them).
|
||||||
pub fn clear_passwords(&mut self) {
|
pub fn clear_passwords(&mut self) {
|
||||||
self.security.open_password.clear();
|
self.security.open_password.clear();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
//! Compatibility PDF 1.3: which standards allow it, the header and metadata that say so, the
|
||||||
|
//! writer refusing transparency (the app flattens it first) and 40-bit RC4 encryption.
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use vectorcraft_color::{BlendMode, Color, Paint};
|
||||||
|
use vectorcraft_doc::{Appearance, Document, Node};
|
||||||
|
use vectorcraft_geom::{Rect, shapes};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// A document with a black rectangle; `opacity` below 1 makes it transparent.
|
||||||
|
fn doc(opacity: f32) -> Document {
|
||||||
|
let mut d = Document::new(100.0, 100.0);
|
||||||
|
let l = d.default_layer().unwrap();
|
||||||
|
let mut n = Node::path(
|
||||||
|
d.alloc_id(),
|
||||||
|
shapes::rectangle(Rect::new(10.0, 10.0, 50.0, 50.0)),
|
||||||
|
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||||
|
);
|
||||||
|
n.opacity = opacity;
|
||||||
|
d.insert(Some(l), 0, n).unwrap();
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(v: serde_json::Value) -> PdfOptions {
|
||||||
|
PdfOptions { settings: serde_json::from_value(v).unwrap(), created: Some(0), ..PdfOptions::uncompressed() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn which_standards_allow_pdf_1_3() {
|
||||||
|
use Compatibility::*;
|
||||||
|
assert!(Standard::None.allows(Pdf13) && Standard::PdfX1a.allows(Pdf13) && Standard::PdfX3.allows(Pdf13));
|
||||||
|
assert!(!Standard::PdfA2b.allows(Pdf13) && !Standard::PdfX4.allows(Pdf13));
|
||||||
|
// PDF/X-1a and PDF/X-3 files are PDF 1.3; PDF 1.4 is still accepted, and written as 1.3.
|
||||||
|
assert_eq!((Standard::PdfX1a.version(), Standard::PdfX3.version()), (Pdf13, Pdf13));
|
||||||
|
assert!(Standard::PdfX1a.allows(Pdf14));
|
||||||
|
let s = |v| serde_json::from_value::<PdfSettings>(v).unwrap();
|
||||||
|
assert!(s(json!({"compatibility": "1.3"})).pdf13() && s(json!({"standard": "pdfX3", "compatibility": "1.4"})).pdf13());
|
||||||
|
assert!(!s(json!({"compatibility": "1.4"})).pdf13() && !s(json!({"standard": "pdfX4"})).pdf13());
|
||||||
|
assert!(matches!(s(json!({"standard": "pdfA2b", "compatibility": "1.3"})).check(), Err(PdfError::BadSetting(m)) if m.contains("PDF 1.3")));
|
||||||
|
assert_eq!(Compatibility::IDS.first(), Some(&"1.3"), "first in the dropdown");
|
||||||
|
assert_eq!(Encryption::for_compatibility(Pdf13), Encryption::Rc440);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_1_3_files_say_so_and_have_no_transparency() {
|
||||||
|
let mut d = doc(1.0);
|
||||||
|
// Isolated page blending is a transparency group: a flat file has none.
|
||||||
|
d.page_isolate = true;
|
||||||
|
let r = export_with_report(&d, &options(json!({"compatibility": "1.3"}))).unwrap();
|
||||||
|
let text = String::from_utf8_lossy(&r.bytes);
|
||||||
|
assert!(r.bytes.starts_with(b"%PDF-1.3") && text.contains("<pdf:PDFVersion>1.3<"), "the header and the metadata");
|
||||||
|
assert!(!text.contains("/Transparency") && !text.contains("/SMask"));
|
||||||
|
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
||||||
|
// The same at 1.4 has its page group.
|
||||||
|
assert!(String::from_utf8_lossy(&export(&d, &options(json!({"compatibility": "1.4"}))).unwrap()).contains("/S/Transparency"));
|
||||||
|
// Transparency the app didn't flatten is refused.
|
||||||
|
let e = export(&doc(0.5), &options(json!({"compatibility": "1.3"}))).unwrap_err();
|
||||||
|
assert!(matches!(&e, PdfError::Unsupported(m) if m.contains("PDF 1.3 files have no transparency")), "{e}");
|
||||||
|
let mut d = doc(1.0);
|
||||||
|
if let Some(n) = d.layers[0].children().and_then(|c| c.first()).map(|n| n.id) {
|
||||||
|
d.node_mut(n).unwrap().blend = BlendMode::Multiply;
|
||||||
|
}
|
||||||
|
assert!(matches!(export(&d, &options(json!({"compatibility": "1.3"}))), Err(PdfError::Unsupported(m)) if m.contains("PDF 1.3")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_1_3_files_are_encrypted_with_40_bit_rc4() {
|
||||||
|
let o = options(json!({"compatibility": "1.3", "security": {"openPassword": "open", "permissionsPassword": "owner", "plaintextMetadata": true}}));
|
||||||
|
let r = export_with_report(&doc(1.0), &o).unwrap();
|
||||||
|
assert!(String::from_utf8_lossy(&r.bytes).contains("/Filter/Standard/V 1/R 2/Length 40/P "));
|
||||||
|
assert!(r.warnings.iter().any(|w| w.contains("PDF 1.3 encryption covers the metadata too")), "{:?}", r.warnings);
|
||||||
|
assert!(matches!(import(&r.bytes), Err(PdfError::NeedsPassword)));
|
||||||
|
for password in ["open", "owner"] {
|
||||||
|
let opened = import_with_report(&r.bytes, &ImportOptions { password: Some(password.into()), ..Default::default() }).unwrap();
|
||||||
|
assert_eq!(opened.document.layers[0].children().map_or(0, |c| c.len()), 1, "{password}");
|
||||||
|
}
|
||||||
|
assert_eq!(crate::encrypt::user_password(&r.bytes, "owner").as_deref(), Some("open"));
|
||||||
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ fn json_is_camel_case_and_partial_objects_keep_defaults() {
|
||||||
assert_eq!(s.compression.gray.downsample, Downsample::Bicubic);
|
assert_eq!(s.compression.gray.downsample, Downsample::Bicubic);
|
||||||
assert_eq!(s.compression.gray.ppi, 300.0);
|
assert_eq!(s.compression.gray.ppi, 300.0);
|
||||||
assert!(s.compression.compress_text);
|
assert!(s.compression.compress_text);
|
||||||
for bad in [json!({"compatibility": "1.3"}), json!({"standard": "pdfX9"}), json!({"marks": {"trim": "yes"}})] {
|
for bad in [json!({"compatibility": "1.2"}), json!({"standard": "pdfX9"}), json!({"marks": {"trim": "yes"}})] {
|
||||||
assert!(serde_json::from_value::<PdfSettings>(bad.clone()).is_err(), "{bad}");
|
assert!(serde_json::from_value::<PdfSettings>(bad.clone()).is_err(), "{bad}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use std::sync::LazyLock;
|
||||||
|
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
use vectorcraft_doc::Unit;
|
use vectorcraft_doc::Unit;
|
||||||
|
use vectorcraft_engine::cmd::FlattenOptions;
|
||||||
use vectorcraft_engine::cmd::fileio::{SaveMode, pdf};
|
use vectorcraft_engine::cmd::fileio::{SaveMode, pdf};
|
||||||
use vectorcraft_pdf::{
|
use vectorcraft_pdf::{
|
||||||
Changes, Choice, ColorConversion, Compatibility, Downsample, Encryption, ImageCodec, JpegQuality, MarkKind, MonoCodec, Overprint, PdfSettings,
|
Changes, Choice, ColorConversion, Compatibility, Downsample, Encryption, ImageCodec, JpegQuality, MarkKind, MonoCodec, Overprint, PdfSettings,
|
||||||
|
|
@ -331,7 +332,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||||
"Compression" => compression(ui, d),
|
"Compression" => compression(ui, d),
|
||||||
"Marks and Bleeds" => marks_and_bleeds(app, ui, d),
|
"Marks and Bleeds" => marks_and_bleeds(app, ui, d),
|
||||||
"Output" => output(ui, d),
|
"Output" => output(ui, d),
|
||||||
"Advanced" => advanced(ui, d),
|
"Advanced" => advanced(app, ui, d),
|
||||||
"Security" => security(ui, d),
|
"Security" => security(ui, d),
|
||||||
"Summary" => summary(app, ui, d),
|
"Summary" => summary(app, ui, d),
|
||||||
_ => general(ui, d, editor),
|
_ => general(ui, d, editor),
|
||||||
|
|
@ -369,8 +370,9 @@ pub(super) fn section_list(ui: &mut egui::Ui, d: &mut Dialog, sections: &[&str],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What choosing `standard` changes: the compatibility, to the latest version it allows, and off
|
/// What choosing `standard` changes: the compatibility, to its version unless it allows the one
|
||||||
/// what files of a standard can't have (editing data, passwords; layers in PDF/X-1a and PDF/X-3).
|
/// chosen, and off what files of a standard can't have (editing data, passwords; layers in PDF/X-1a
|
||||||
|
/// and PDF/X-3).
|
||||||
fn standard_chosen(d: &mut Dialog, standard: Standard) {
|
fn standard_chosen(d: &mut Dialog, standard: Standard) {
|
||||||
if choice::<Compatibility>(d, "compatibility").is_some_and(|c| !standard.allows(c)) {
|
if choice::<Compatibility>(d, "compatibility").is_some_and(|c| !standard.allows(c)) {
|
||||||
set(d, "compatibility", json!(standard.version().id()));
|
set(d, "compatibility", json!(standard.version().id()));
|
||||||
|
|
@ -604,7 +606,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advanced(ui: &mut egui::Ui, d: &mut Dialog) {
|
fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||||
heading(ui, "Fonts");
|
heading(ui, "Fonts");
|
||||||
let outline = get(d, "advanced.outlineText").as_bool() == Some(true);
|
let outline = get(d, "advanced.outlineText").as_bool() == Some(true);
|
||||||
row(ui, "Subset fonts below:", |ui| {
|
row(ui, "Subset fonts below:", |ui| {
|
||||||
|
|
@ -612,10 +614,28 @@ fn advanced(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||||
ui.label("of characters used");
|
ui.label("of characters used");
|
||||||
});
|
});
|
||||||
flag(ui, d, "advanced.outlineText", "Convert text to outlines", true);
|
flag(ui, d, "advanced.outlineText", "Convert text to outlines", true);
|
||||||
heading(ui, "Overprint");
|
heading(ui, "Overprint and Transparency Flattener");
|
||||||
row(ui, "Overprint:", |ui| {
|
row(ui, "Overprint:", |ui| {
|
||||||
pick::<Overprint>(ui, d, "advanced.overprint", 130.0, |_| true);
|
pick::<Overprint>(ui, d, "advanced.overprint", 130.0, |_| true);
|
||||||
});
|
});
|
||||||
|
// PDF 1.3 files (PDF/X-1a and PDF/X-3 ones too) have no transparency: it is flattened.
|
||||||
|
let flat = choice::<Compatibility>(d, "compatibility") == Some(Compatibility::Pdf13)
|
||||||
|
|| choice::<Standard>(d, "standard").is_some_and(Standard::flattens);
|
||||||
|
row(ui, "Flattener preset:", |ui| {
|
||||||
|
let presets = app.session.flattener_presets();
|
||||||
|
let names: Vec<&str> = presets.iter().map(|p| p.name.as_str()).collect();
|
||||||
|
let name = get(d, "flattenerPreset").as_str().map(str::trim).filter(|n| !n.is_empty()).unwrap_or(pdf::DEFAULT_FLATTENER);
|
||||||
|
// A built-in preset named by id (`high`) shows as its name.
|
||||||
|
let current = FlattenOptions::preset_label(&name.to_ascii_lowercase()).unwrap_or(name).to_string();
|
||||||
|
ui.add_enabled_ui(flat, |ui| {
|
||||||
|
if let Some(n) = widgets::dropdown(ui, "flattenerPreset", ¤t, &names, 200.0).and_then(|i| names.get(i)) {
|
||||||
|
set(d, "flattenerPreset", json!(n));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if !flat {
|
||||||
|
note(ui, "PDF 1.3 files have no transparency: the preset flattens it.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The password fields (never stored in presets).
|
/// The password fields (never stored in presets).
|
||||||
|
|
@ -670,7 +690,13 @@ fn security(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||||
flag(ui, d, "security.screenReader", reader, restrict);
|
flag(ui, d, "security.screenReader", reader, restrict);
|
||||||
}
|
}
|
||||||
let compatibility = choice::<Compatibility>(d, "compatibility").unwrap_or_default();
|
let compatibility = choice::<Compatibility>(d, "compatibility").unwrap_or_default();
|
||||||
flag(ui, d, "security.plaintextMetadata", "Enable plaintext metadata", (open || restrict) && compatibility != Compatibility::Pdf14);
|
flag(
|
||||||
|
ui,
|
||||||
|
d,
|
||||||
|
"security.plaintextMetadata",
|
||||||
|
"Enable plaintext metadata",
|
||||||
|
(open || restrict) && !matches!(compatibility, Compatibility::Pdf13 | Compatibility::Pdf14),
|
||||||
|
);
|
||||||
ui.add_space(6.0);
|
ui.add_space(6.0);
|
||||||
note(ui, &format!("Encryption: {} (set by Compatibility)", Encryption::for_compatibility(compatibility).label()));
|
note(ui, &format!("Encryption: {} (set by Compatibility)", Encryption::for_compatibility(compatibility).label()));
|
||||||
}
|
}
|
||||||
|
|
@ -966,7 +992,7 @@ mod tests {
|
||||||
let d = app.ui.dialog.as_mut().unwrap();
|
let d = app.ui.dialog.as_mut().unwrap();
|
||||||
set(d, "standard", json!(Standard::PdfX1a.id()));
|
set(d, "standard", json!(Standard::PdfX1a.id()));
|
||||||
standard_chosen(d, Standard::PdfX1a);
|
standard_chosen(d, Standard::PdfX1a);
|
||||||
assert_eq!((get(d, "compatibility"), get(d, "createLayers"), get(d, "preserveEditing")), (&json!("1.4"), &json!(false), &json!(false)));
|
assert_eq!((get(d, "compatibility"), get(d, "createLayers"), get(d, "preserveEditing")), (&json!("1.3"), &json!(false), &json!(false)));
|
||||||
for s in SECTIONS {
|
for s in SECTIONS {
|
||||||
set_field(&mut app, "__section", json!(s));
|
set_field(&mut app, "__section", json!(s));
|
||||||
frame(&mut app);
|
frame(&mut app);
|
||||||
|
|
@ -1000,4 +1026,23 @@ mod tests {
|
||||||
// The empty pages of the other artboards come in as page layers.
|
// The empty pages of the other artboards come in as page layers.
|
||||||
assert_eq!(names[..2], ["Layer 1", "Notes"]);
|
assert_eq!(names[..2], ["Layer 1", "Notes"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pdf_1_3_takes_a_flattener_preset_in_advanced() {
|
||||||
|
let (mut app, written, _) = app();
|
||||||
|
app.run("ui.savePdfDialog", json!({"path": "/tmp/old.pdf", "compatibility": "1.3"})).unwrap();
|
||||||
|
for s in ["Advanced", "Security"] {
|
||||||
|
set_field(&mut app, "__section", json!(s));
|
||||||
|
frame(&mut app);
|
||||||
|
}
|
||||||
|
set_field(&mut app, "flattenerPreset", json!("Low Resolution"));
|
||||||
|
let p = params(app.ui.dialog.as_ref().unwrap());
|
||||||
|
assert_eq!((p["compatibility"].as_str(), p["flattenerPreset"].as_str()), (Some("1.3"), Some("Low Resolution")));
|
||||||
|
super::super::confirm(&mut app).unwrap();
|
||||||
|
assert!(written.borrow()[0].1.starts_with(b"%PDF-1.3"));
|
||||||
|
// A preset keeps the flattener preset.
|
||||||
|
app.run("pdf.preset.save", json!({"name": "Old", "compatibility": "1.3", "flattenerPreset": "Low Resolution"})).unwrap();
|
||||||
|
let saved = app.session.prefs.pdf_presets.iter().find(|p| p.name == "Old").unwrap();
|
||||||
|
assert_eq!((saved.settings.compatibility, saved.settings.flattener_preset.as_str()), (Compatibility::Pdf13, "Low Resolution"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
docs/mcp.md
21
docs/mcp.md
|
|
@ -216,10 +216,10 @@ PDF files take the Save PDF dialog's options: `preset`, `standard`, `compatibili
|
||||||
`compression`, `marks`, `bleed`, `output`, `advanced` and `security` sections (`list_commands` with filter `exportPdf`
|
`compression`, `marks`, `bleed`, `output`, `advanced` and `security` sections (`list_commands` with filter `exportPdf`
|
||||||
documents every field). `document.exportPdf` and `export` (format `pdf`, the options in `options`) return `warnings`:
|
documents every field). `document.exportPdf` and `export` (format `pdf`, the options in `options`) return `warnings`:
|
||||||
options accepted but not applied yet, and features approximated or left out. A standard with a PDF version it doesn't
|
options accepted but not applied yet, and features approximated or left out. A standard with a PDF version it doesn't
|
||||||
allow is refused (PDF/A-2b at 2.0, PDF/X-4 above 1.6, PDF/X-1a and PDF/X-3 at anything but 1.4); choosing a standard
|
allow is refused (PDF/A-2b at 1.3 or 2.0, PDF/X-4 above 1.6, PDF/X-1a and PDF/X-3 above 1.4); choosing a
|
||||||
without giving `compatibility` sets the latest it allows.
|
standard without giving `compatibility` sets its version (PDF 1.3 for PDF/X-1a and PDF/X-3).
|
||||||
A `security` password encrypts the file (RC4 128-bit at PDF 1.4–1.5, AES-128 at 1.6, AES-256 at 1.7 and 2.0) with its
|
A `security` password encrypts the file (RC4 40-bit at PDF 1.3, RC4 128-bit at 1.4–1.5, AES-128 at 1.6, AES-256 at
|
||||||
permissions; either password opens it (`document.open {password}`), and a password with PDF/A or PDF/X is refused.
|
1.7 and 2.0) with its permissions; either password opens it (`document.open {password}`), and a password with PDF/A or PDF/X is refused.
|
||||||
Pattern fills and strokes are written as their tiles clipped to the area they paint (a stroke's outline, with its
|
Pattern fills and strokes are written as their tiles clipped to the area they paint (a stroke's outline, with its
|
||||||
dashes, caps, profile, arrowheads and alignment), and freeform gradients as an image of their colour field at the
|
dashes, caps, profile, arrowheads and alignment), and freeform gradients as an image of their colour field at the
|
||||||
document's raster effects resolution, clipped the same way.
|
document's raster effects resolution, clipped the same way.
|
||||||
|
|
@ -234,6 +234,11 @@ lossless). `none`, `jpeg2000`, CCITT and `runLength` are written as ZIP, with a
|
||||||
`thumbnails: true` embeds each page drawn small (106 px on its long side, without the layers the page leaves out) as
|
`thumbnails: true` embeds each page drawn small (106 px on its long side, without the layers the page leaves out) as
|
||||||
its `/Thumb` image. `fastWebView: true` writes a linearised file (the linearization dictionary first, then the first
|
its `/Thumb` image. `fastWebView: true` writes a linearised file (the linearization dictionary first, then the first
|
||||||
page with hint tables saying where every other page's objects are), which stays linearised when a password encrypts it.
|
page with hint tables saying where every other page's objects are), which stays linearised when a password encrypts it.
|
||||||
|
`compatibility: "1.3"` writes a PDF 1.3 file, which has no transparency: a copy of the document is flattened first
|
||||||
|
with `flattenerPreset` (empty: High Resolution; `high`, `medium`, `low` or a saved preset, `flattener.presets.list`)
|
||||||
|
and `flattener` options over it (as `object.flattenTransparency` takes them); images with see-through pixels count as
|
||||||
|
transparency and rasterized areas are clipped to their regions. A file that would still have transparency fails the
|
||||||
|
export. PDF/X-1a and PDF/X-3 files are PDF 1.3 files too.
|
||||||
|
|
||||||
Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based),
|
Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based),
|
||||||
`cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets)
|
`cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets)
|
||||||
|
|
@ -1585,10 +1590,10 @@ layers are refused.
|
||||||
- **pdfX4** (PDF/X-4:2010, PDF 1.6 at most): transparency and layers are kept, colours are tagged, and the XMP metadata
|
- **pdfX4** (PDF/X-4:2010, PDF 1.6 at most): transparency and layers are kept, colours are tagged, and the XMP metadata
|
||||||
names the standard.
|
names the standard.
|
||||||
|
|
||||||
Flattening uses the High Resolution flattener preset on a copy (the document is untouched; a warning says so), and
|
Flattening uses `flattenerPreset` (default High Resolution, see PDF 1.3 above) on a copy (the document is untouched; a
|
||||||
images with see-through pixels count as transparency. The written file is checked against its standard: what it still
|
warning says so), and images with see-through pixels count as transparency. The written file is checked against
|
||||||
breaks (transparency, RGB in PDF/X-1a, a font not embedded) fails the export instead of writing a file that only
|
its standard: what it still breaks (transparency, RGB in PDF/X-1a, a font not embedded) fails the export instead of
|
||||||
claims the standard.
|
writing a file that only claims the standard.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/press.pdf","preset":"PDF/X-1a:2001","output":{"outputIntent":"VectorCraft Generic CMYK (SWOP-like)","outputCondition":"Coated","trapped":false}}}}
|
{"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/press.pdf","preset":"PDF/X-1a:2001","output":{"outputIntent":"VectorCraft Generic CMYK (SWOP-like)","outputCondition":"Coated","trapped":false}}}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue