Land #603 (part of #372: .ai layers and sublayers as PDF layers, hidden ones kept)
This commit is contained in:
commit
c3b73f0146
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -306,11 +306,16 @@ fn blank_pages(doc: &Document) -> Document {
|
|||
const NOT_PDF_COMPATIBLE: &str = "saved without PDF content: VectorCraft opens it as before, other apps show empty pages";
|
||||
|
||||
/// The encoder's params for a `.ai` file, from a save's or an export's: a PDF of every artboard
|
||||
/// with the PDF options, always carrying the native document; Use Compression compresses the
|
||||
/// PDF's content.
|
||||
/// with the PDF options, always carrying the native document, its layers and sublayers PDF layers
|
||||
/// (hidden ones off) unless asked otherwise; Use Compression compresses the PDF's content.
|
||||
pub(super) fn ai_params(params: &mut Value) {
|
||||
let Some(o) = params.as_object_mut() else { return };
|
||||
o.extend([("preserveEditing".into(), json!(true)), ("range".into(), json!("all"))]);
|
||||
// Apps that read the PDF part (not the native document) then find the layers, the hidden ones
|
||||
// with their art (#372); a PDF standard decides for itself.
|
||||
if o.get("standard").is_none_or(Value::is_null) {
|
||||
o.entry("createLayers").or_insert(json!(true));
|
||||
}
|
||||
if let Some(c) = o.get("compress").and_then(Value::as_bool) {
|
||||
let compression = o.entry("compression").or_insert_with(|| json!({}));
|
||||
if let Some(m) = compression.as_object_mut() {
|
||||
|
|
|
|||
|
|
@ -59,3 +59,35 @@ fn export_pdf_overprint_is_preserved_or_discarded() {
|
|||
assert_eq!(overprints(&mut s, json!({"compression": {"compressText": false}})), 1);
|
||||
assert_eq!(overprints(&mut s, json!({"advanced": {"overprint": "discard"}})), 0);
|
||||
}
|
||||
|
||||
/// A `.ai` file's PDF part, which apps that don't read the native document open, has the layers
|
||||
/// and sublayers as PDF layers, the hidden ones with their art (#372).
|
||||
#[test]
|
||||
fn ai_files_write_their_layers_and_sublayers_as_pdf_layers() {
|
||||
let mut s = session();
|
||||
let layer1 = s.execute("document.inspect", &json!({})).unwrap()["layers"][1]["id"].as_u64().unwrap();
|
||||
let sub = s.execute("layer.newSublayer", &json!({"parent": layer1, "name": "Sketch"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("layer.setCurrent", &json!({"id": sub})).unwrap();
|
||||
s.execute("shape.ellipse", &json!({"x": 10, "y": 40, "width": 20, "height": 20})).unwrap();
|
||||
s.execute("layer.setProps", &json!({"id": sub, "visible": false})).unwrap();
|
||||
// Name, shown and what it holds (sublayers by name), top-level layers in paint order.
|
||||
fn rows(layers: &[std::sync::Arc<vectorcraft_doc::Node>]) -> Vec<(String, bool, Vec<String>)> {
|
||||
layers
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let held =
|
||||
l.children().into_iter().flatten().map(|c| c.name.clone().filter(|_| c.is_layer()).unwrap_or_else(|| "art".into())).collect();
|
||||
(l.name.clone().unwrap_or_default(), l.visible, held)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
let v = s.execute("document.export", &json!({"format": "ai"})).unwrap();
|
||||
let back = vectorcraft_pdf::import(&b64(&v)).unwrap();
|
||||
let art = || "art".to_string();
|
||||
assert_eq!(rows(&back.layers), [("Layer 1".to_string(), true, vec![art(), "Sketch".into()]), ("Notes".to_string(), false, vec![art()])]);
|
||||
let sketch = back.layers.first().and_then(|l| l.children()?.last().cloned()).unwrap();
|
||||
assert_eq!(rows(&[sketch]), [("Sketch".to_string(), false, vec![art()])]);
|
||||
// Asked not to, it writes none.
|
||||
let v = s.execute("document.export", &json!({"format": "ai", "createLayers": false})).unwrap();
|
||||
assert_eq!(vectorcraft_pdf::import(&b64(&v)).unwrap().layers.len(), 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -453,11 +453,13 @@ pub(crate) struct Exporter<'a> {
|
|||
/// The fonts real text is written in, by face id, with their units per em; `None` for faces
|
||||
/// that can't be embedded.
|
||||
fonts: HashMap<u32, Option<(krilla::text::Font, f64)>>,
|
||||
/// Top-level layers are drawn as forms marked for their optional content groups
|
||||
/// Layers and sublayers are drawn as forms marked for their optional content groups
|
||||
/// ([`crate::forms`]).
|
||||
pub layers: bool,
|
||||
/// A top-level layer's form is being drawn.
|
||||
in_layer: bool,
|
||||
/// Each layer's index in [`crate::forms::pdf_layers`], by address.
|
||||
layer_index: HashMap<usize, usize>,
|
||||
/// The address of the layer whose form is being drawn.
|
||||
in_layer: usize,
|
||||
/// Overprinting fills and strokes are drawn as forms marked to overprint.
|
||||
pub overprint: bool,
|
||||
/// An overprinting fill or stroke was drawn so.
|
||||
|
|
@ -485,7 +487,8 @@ impl<'a> Exporter<'a> {
|
|||
outline_text: set.advanced.outline_text,
|
||||
fonts: HashMap::new(),
|
||||
layers: false,
|
||||
in_layer: false,
|
||||
layer_index: crate::forms::pdf_layers(doc).iter().enumerate().map(|(i, l)| (address(l.node), i)).collect(),
|
||||
in_layer: 0,
|
||||
overprint: false,
|
||||
overprinted: false,
|
||||
}
|
||||
|
|
@ -601,6 +604,11 @@ fn constant_mask(s: &mut Surface, page: Rect, alpha: f32) -> krilla::mask::Mask
|
|||
krilla::mask::Mask::new(sb.finish(), krilla::mask::MaskType::Alpha)
|
||||
}
|
||||
|
||||
/// A node's address, to tell the very node apart from equal ones.
|
||||
fn address(n: &Node) -> usize {
|
||||
std::ptr::from_ref(n).addr()
|
||||
}
|
||||
|
||||
fn rects_overlap(a: Rect, b: Rect) -> bool {
|
||||
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
|
||||
}
|
||||
|
|
@ -885,15 +893,15 @@ impl Exporter<'_> {
|
|||
|
||||
fn node(&mut self, s: &mut Surface, n: &Node, page: Rect, force: bool) {
|
||||
if self.layers
|
||||
&& !self.in_layer
|
||||
&& let NodeKind::Layer { template: false, printable, .. } = n.kind
|
||||
&& self.in_layer != address(n)
|
||||
&& let NodeKind::Layer { printable, .. } = n.kind
|
||||
&& (printable || self.non_printing)
|
||||
&& let Some(i) = self.doc.layers.iter().position(|l| std::ptr::eq(&**l, n))
|
||||
&& let Some(&i) = self.layer_index.get(&address(n))
|
||||
{
|
||||
// A PDF layer: hidden layers are written too (their group is off).
|
||||
self.in_layer = true;
|
||||
// A PDF layer: hidden layers and sublayers are written too (their group is off).
|
||||
let enclosing = std::mem::replace(&mut self.in_layer, address(n));
|
||||
crate::forms::form(s, Mark::Layer(i), |s| self.node(s, n, page, true));
|
||||
self.in_layer = false;
|
||||
self.in_layer = enclosing;
|
||||
return;
|
||||
}
|
||||
if !force && !n.visible {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,9 @@ use std::io::Read;
|
|||
use krilla::geom::PathBuilder;
|
||||
use krilla::graphic::Graphic;
|
||||
use krilla::surface::Surface;
|
||||
use vectorcraft_doc::{Document, NodeKind};
|
||||
use std::sync::Arc;
|
||||
|
||||
use vectorcraft_doc::{Document, Node, NodeKind};
|
||||
|
||||
use crate::encrypt::{Lexer, Obj, StreamSpans, Tok, int, stream_spans};
|
||||
use crate::output::{deflate, text_string};
|
||||
|
|
@ -21,12 +23,36 @@ use crate::{PdfError, PdfSettings, Standard};
|
|||
/// What a marked form is.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Mark {
|
||||
/// The art of top-level layer `i` (`Document::layers`).
|
||||
/// The art of layer `i` of [`pdf_layers`].
|
||||
Layer(usize),
|
||||
/// A fill or stroke that overprints.
|
||||
Overprint,
|
||||
}
|
||||
|
||||
/// A layer written as a PDF layer (an optional content group).
|
||||
pub(crate) struct PdfLayer<'a> {
|
||||
pub node: &'a Node,
|
||||
/// Its parent layer's index in [`pdf_layers`] (none for a top-level layer).
|
||||
pub parent: Option<usize>,
|
||||
}
|
||||
|
||||
/// The layers written as PDF layers: the top-level layers and their sublayers, but template layers
|
||||
/// (and what they hold), each before its sublayers, in paint order (bottom first).
|
||||
pub(crate) fn pdf_layers(doc: &Document) -> Vec<PdfLayer<'_>> {
|
||||
fn add<'a>(nodes: &'a [Arc<Node>], parent: Option<usize>, out: &mut Vec<PdfLayer<'a>>) {
|
||||
for n in nodes {
|
||||
if let NodeKind::Layer { template: false, children, .. } = &n.kind {
|
||||
let i = out.len();
|
||||
out.push(PdfLayer { node: n, parent });
|
||||
add(children, Some(i), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = vec![];
|
||||
add(&doc.layers, None, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// The x coordinate of a layer mark's first point (its y is the layer index).
|
||||
const LAYER_X: i32 = -32000;
|
||||
/// The x coordinate of an overprint mark's first point.
|
||||
|
|
@ -199,7 +225,7 @@ fn mark_layers(content: &[u8], forms: &HashMap<&[u8], usize>) -> Option<(Vec<u8>
|
|||
}
|
||||
|
||||
/// Finish the marked forms of `pdf` (as [`crate::export`] writes `doc` with `set`): with
|
||||
/// `layers`, each top-level layer (but template layers) becomes an optional content group —
|
||||
/// `layers`, each layer and sublayer (but template layers) becomes an optional content group —
|
||||
/// named as the layer, off when it is hidden, not printed (`/PrintState /OFF`) when its Print
|
||||
/// option is off, locked when it is locked — and the uses of its forms its optional content;
|
||||
/// with `overprint`, the overprinting forms get a graphics state that overprints (`/OP`, `/op`,
|
||||
|
|
@ -228,6 +254,11 @@ pub(crate) fn finish(pdf: Vec<u8>, doc: &Document, set: &PdfSettings, layers: bo
|
|||
}
|
||||
if layers {
|
||||
write_layers(&pdf, &xref, &objects, &marks, &mut patch, doc, set)?;
|
||||
for (n, _) in marks.iter().filter(|(_, m)| matches!(m, Mark::Layer(_))) {
|
||||
if let Some(o) = objects.get(n) {
|
||||
unbox(&pdf, o, &mut patch)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let pdf = patch.apply(&pdf, &xref)?;
|
||||
// The file still is what its standard says.
|
||||
|
|
@ -235,8 +266,9 @@ pub(crate) fn finish(pdf: Vec<u8>, doc: &Document, set: &PdfSettings, layers: bo
|
|||
Ok(pdf)
|
||||
}
|
||||
|
||||
/// The optional content groups of `doc`'s top-level layers, the uses of their forms marked as
|
||||
/// their content, and the catalog's `/OCProperties`.
|
||||
/// The optional content groups of `doc`'s layers and sublayers ([`pdf_layers`]), the uses of their
|
||||
/// forms marked as their content, and the catalog's `/OCProperties`, listing sublayers under their
|
||||
/// layers.
|
||||
fn write_layers(
|
||||
pdf: &[u8],
|
||||
xref: &Xref,
|
||||
|
|
@ -247,11 +279,16 @@ fn write_layers(
|
|||
set: &PdfSettings,
|
||||
) -> Result<(), PdfError> {
|
||||
// One group per layer, in paint order (bottom first).
|
||||
let layers = pdf_layers(doc);
|
||||
let mut groups: HashMap<usize, u32> = HashMap::new();
|
||||
let (mut all, mut off, mut locked) = (vec![], vec![], vec![]);
|
||||
for (i, l) in doc.layers.iter().enumerate() {
|
||||
let NodeKind::Layer { template: false, printable, .. } = l.kind else { continue };
|
||||
let name = l.name.clone().unwrap_or_else(|| format!("Layer {}", i + 1));
|
||||
// Each layer's sublayers, and the top-level layers, in paint order.
|
||||
let mut children: HashMap<Option<usize>, Vec<usize>> = HashMap::new();
|
||||
for (i, PdfLayer { node: l, parent }) in layers.iter().enumerate() {
|
||||
let siblings = children.entry(*parent).or_default();
|
||||
siblings.push(i);
|
||||
let name = l.name.clone().unwrap_or_else(|| format!("Layer {}", siblings.len()));
|
||||
let printable = matches!(l.kind, NodeKind::Layer { printable: true, .. });
|
||||
let on = |b: bool| if b { "ON" } else { "OFF" };
|
||||
let g = patch.add_object(
|
||||
format!("<</Type/OCG/Name{}/Usage<</View<</ViewState/{}>>/Print<</PrintState/{}>>>>>>", text_string(&name), on(l.visible), on(printable))
|
||||
|
|
@ -295,9 +332,7 @@ fn write_layers(
|
|||
add_resources(patch, res, "Properties", &entries)?;
|
||||
}
|
||||
let refs = |v: &[u32]| v.iter().map(|g| format!("{g} 0 R")).collect::<Vec<_>>().join(" ");
|
||||
// Layers panels list the top layer first.
|
||||
let order: Vec<u32> = all.iter().rev().copied().collect();
|
||||
let mut config = format!("/Name(Layers)/Order[{}]", refs(&order));
|
||||
let mut config = format!("/Name(Layers)/Order[{}]", order(&children, &groups, None));
|
||||
if !off.is_empty() {
|
||||
config.push_str(&format!("/OFF[{}]", refs(&off)));
|
||||
}
|
||||
|
|
@ -313,3 +348,35 @@ fn write_layers(
|
|||
patch.replace(start + 2, start + 2, format!("/OCProperties<</OCGs[{}]/D<<{config}>>>>", refs(&all)).into_bytes());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A form's box as large as a page can be: the box of a layer's form is only the bounds of its
|
||||
/// art, and a reader that makes a clipping group of a form's box (as importers do) would gather a
|
||||
/// layer's art and its sublayers' in one group, which no layer can hold. A box round the whole
|
||||
/// page clips nothing.
|
||||
const UNBOXED: &[u8] = b"[-32767 -32767 32767 32767]";
|
||||
|
||||
/// Give the layer's form `o` the [`UNBOXED`] box.
|
||||
fn unbox(pdf: &[u8], o: &Object<'_>, patch: &mut Patch) -> Result<(), PdfError> {
|
||||
let Obj::Dict { start, end, .. } = o.dict else { return Err(bad("a layer's form has no dictionary")) };
|
||||
let dict = pdf.get(start..end).ok_or_else(|| bad("a layer's form can't be read"))?;
|
||||
// The writer writes the box as an array of numbers.
|
||||
let at = dict.windows(5).position(|w| w == b"/BBox").ok_or_else(|| bad("a layer's form has no box"))? + 5;
|
||||
let open = at + dict.get(at..).and_then(|d| d.iter().position(|&c| c == b'[')).ok_or_else(|| bad("a layer's form box isn't an array"))?;
|
||||
let close = open + dict.get(open..).and_then(|d| d.iter().position(|&c| c == b']')).ok_or_else(|| bad("a layer's form box isn't an array"))? + 1;
|
||||
patch.replace(start + open, start + close, UNBOXED.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `/Order` entries of `parent`'s sublayers (the top-level layers for none), the top one first
|
||||
/// as Layers panels list them, each followed by an array of its own sublayers when it has some.
|
||||
fn order(children: &HashMap<Option<usize>, Vec<usize>>, groups: &HashMap<usize, u32>, parent: Option<usize>) -> String {
|
||||
let mut out = vec![];
|
||||
for &i in children.get(&parent).into_iter().flatten().rev() {
|
||||
let Some(g) = groups.get(&i) else { continue };
|
||||
out.push(format!("{g} 0 R"));
|
||||
if children.contains_key(&Some(i)) {
|
||||
out.push(format!("[{}]", order(children, groups, Some(i))));
|
||||
}
|
||||
}
|
||||
out.join(" ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -537,14 +537,23 @@ struct Frame {
|
|||
children: Vec<Arc<Node>>,
|
||||
/// The first optional content group entered inside (where the frame's art goes).
|
||||
group: Option<usize>,
|
||||
/// The group each child was drawn in, where the frame's art goes when the frame dissolves
|
||||
/// into its parent (a layer's form drawing its sublayers' forms).
|
||||
child_groups: Vec<Option<usize>>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
fn new(kind: FrameKind) -> Self {
|
||||
Self { kind, children: vec![], group: None }
|
||||
Self { kind, children: vec![], group: None, child_groups: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
/// A dissolving frame's `children`, each with the group it was drawn in (`groups`, else the
|
||||
/// frame's `group`).
|
||||
fn grouped(children: Vec<Arc<Node>>, groups: Vec<Option<usize>>, group: Option<usize>) -> impl Iterator<Item = (Arc<Node>, Option<usize>)> {
|
||||
children.into_iter().zip(groups.into_iter().map(move |g| g.or(group)).chain(std::iter::repeat(group)))
|
||||
}
|
||||
|
||||
/// Glyphs drawn consecutively with the same paint, merged into one path.
|
||||
struct GlyphRun {
|
||||
path: BezPath,
|
||||
|
|
@ -846,8 +855,11 @@ impl<'p> Builder<'p> {
|
|||
// A frame's art goes where its first art was drawn (a clip can end in a later group).
|
||||
if root {
|
||||
self.root_groups.push(group.or(current));
|
||||
} else if f.group.is_none() {
|
||||
f.group = group.or(current);
|
||||
} else {
|
||||
f.child_groups.push(group.or(current));
|
||||
if f.group.is_none() {
|
||||
f.group = group.or(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -925,7 +937,7 @@ impl<'p> Builder<'p> {
|
|||
let node = match f.kind {
|
||||
FrameKind::Root => None,
|
||||
FrameKind::Skip => {
|
||||
f.children.into_iter().for_each(|c| self.emit(c, group));
|
||||
grouped(f.children, f.child_groups, group).for_each(|(c, g)| self.emit(c, g));
|
||||
None
|
||||
}
|
||||
FrameKind::Clip(clip) => {
|
||||
|
|
@ -934,7 +946,7 @@ impl<'p> Builder<'p> {
|
|||
None
|
||||
} else if noop.is_some_and(|r| bounds(&f.children).is_some_and(|b| contains(r, b))) {
|
||||
// A rectangle around all of its art (a form's box) clips nothing.
|
||||
f.children.into_iter().for_each(|c| self.emit(c, group));
|
||||
grouped(f.children, f.child_groups, group).for_each(|(c, g)| self.emit(c, g));
|
||||
None
|
||||
} else {
|
||||
let mut ch = vec![Arc::new(*clip)];
|
||||
|
|
@ -957,7 +969,7 @@ impl<'p> Builder<'p> {
|
|||
if children.is_empty() {
|
||||
None
|
||||
} else if plain {
|
||||
children.into_iter().for_each(|c| self.emit(c, group));
|
||||
grouped(children, f.child_groups, group).for_each(|(c, g)| self.emit(c, g));
|
||||
None
|
||||
} else if let [only] = children.as_slice()
|
||||
&& !only.is_container()
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
//! text in embedded subset fonts (Advanced). Colours are converted, tagged with ICC profiles and
|
||||
//! given an output intent as the Output settings say. Hidden objects,
|
||||
//! guides and template layers are skipped, and so are non-printing layers unless asked for.
|
||||
//! Top-level layers can be PDF layers (optional content groups with their visibility, print
|
||||
//! Layers and sublayers can be PDF layers (optional content groups with their visibility, print
|
||||
//! state and lock), and overprinting fills and strokes overprint (Advanced).
|
||||
//! Each page is its artboard (the trim box) grown by the bleed (the bleed box) and by the
|
||||
//! printer's marks around it (the media box), drawn in Registration. [`PdfSettings`] is the Save PDF
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ pub struct PdfSettings {
|
|||
pub fast_web_view: bool,
|
||||
/// The app opens the written file (frontends only; the writer ignores it).
|
||||
pub view_after_saving: bool,
|
||||
/// Write top-level layers as PDF layers (optional content).
|
||||
/// Write layers and sublayers as PDF layers (optional content).
|
||||
pub create_layers: bool,
|
||||
/// Keep the layers whose Print option is off (they are left out otherwise, unless
|
||||
/// `create_layers` is on).
|
||||
|
|
@ -449,7 +449,7 @@ impl PdfSettings {
|
|||
within("advanced.fontSubsetPercent", self.advanced.font_subset_percent, 0.0, 100.0, "%")
|
||||
}
|
||||
|
||||
/// Top-level layers are written as PDF layers: asked for, at a version that has them.
|
||||
/// Layers and sublayers are written as PDF layers: asked for, at a version that has them.
|
||||
pub fn writes_layers(&self) -> bool {
|
||||
self.create_layers && self.compatibility.has_layers()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,57 @@ fn pdf_layers_import_back_as_layers() {
|
|||
}
|
||||
}
|
||||
|
||||
/// The rows of `nodes` in paint order, depth first: `depth name visible` for layers, `depth fill`
|
||||
/// for art.
|
||||
fn tree(nodes: &[std::sync::Arc<Node>], depth: usize, out: &mut Vec<String>) {
|
||||
for n in nodes {
|
||||
match &n.kind {
|
||||
NodeKind::Layer { children, .. } => {
|
||||
out.push(format!("{depth} {} {}", n.name.clone().unwrap_or_default(), n.visible));
|
||||
tree(children, depth + 1, out);
|
||||
}
|
||||
_ => out.push(format!("{depth} {:?}", n.appearance.fill().and_then(|f| f.paint.color()))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sublayers are PDF layers listed under their layer's, hidden ones off with their art, and they
|
||||
/// import back as sublayers (#372: other apps read a `.ai` file's PDF part).
|
||||
#[test]
|
||||
fn sublayers_are_nested_pdf_layers_and_import_back() {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
d.layers.clear();
|
||||
let layer = |d: &mut Document, name: &str, visible: bool, children: Vec<Node>| {
|
||||
let mut l = Node::layer(d.alloc_id(), name, LayerColor::Preset(0));
|
||||
l.visible = visible;
|
||||
if let NodeKind::Layer { children: c, .. } = &mut l.kind {
|
||||
c.extend(children.into_iter().map(std::sync::Arc::new));
|
||||
}
|
||||
l
|
||||
};
|
||||
let deep = vec![rect(&mut d, 90.0, GREEN)];
|
||||
let deep = layer(&mut d, "Deep", true, deep);
|
||||
let sub = vec![rect(&mut d, 50.0, BLUE), deep];
|
||||
let sub = layer(&mut d, "Sub", false, sub);
|
||||
let main = vec![rect(&mut d, 10.0, RED), sub];
|
||||
let main = layer(&mut d, "Main", true, main);
|
||||
d.layers.push(main.into());
|
||||
let r = export(&d, json!({"createLayers": true, "compression": {"compressText": false}}));
|
||||
oc_properties(&r.bytes, None, |props, xref| {
|
||||
let props = props.expect("/OCProperties");
|
||||
assert_eq!(names(xref, props.get(b"OCGs")), ["Main", "Sub", "Deep"]);
|
||||
assert_eq!(names(xref, props.get::<Dict<'_>>(b"D").unwrap().get(b"OFF")), ["Sub"]);
|
||||
});
|
||||
let text = String::from_utf8_lossy(&r.bytes);
|
||||
let order = text.split("/Order").nth(1).and_then(|o| o.split("]/").next()).unwrap().to_string();
|
||||
assert_eq!(order.matches('[').count(), 3, "each layer's sublayers in an array after it: {order}");
|
||||
let mut got = vec![];
|
||||
tree(&import(&r.bytes).unwrap().layers, 0, &mut got);
|
||||
// Paint order, as it was.
|
||||
let fill = |depth, c: Color| format!("{depth} {:?}", Some(c));
|
||||
assert_eq!(got, ["0 Main true".to_string(), fill(1, RED), "1 Sub false".into(), fill(2, BLUE), "2 Deep true".into(), fill(3, GREEN)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_layers_stay_apart() {
|
||||
let mut d = Document::new(200.0, 100.0);
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ fn general(ui: &mut egui::Ui, d: &mut Dialog, editor: bool) {
|
|||
flag(ui, d, "viewAfterSaving", tl!("View PDF after saving"), true);
|
||||
// PDF layers need PDF 1.5, and PDF/X-1a and PDF/X-3 have none.
|
||||
let layers = standard.allows_layers() && choice::<Compatibility>(d, "compatibility").unwrap_or_default().has_layers();
|
||||
flag(ui, d, "createLayers", tl!("Create PDF layers from top-level layers"), layers);
|
||||
flag(ui, d, "createLayers", tl!("Create PDF layers from layers and sublayers"), layers);
|
||||
flag(ui, d, "includeNonPrinting", tl!("Include non-printing layers"), true);
|
||||
if editor {
|
||||
heading(ui, tl!("Description"));
|
||||
|
|
|
|||
|
|
@ -508,7 +508,7 @@
|
|||
Create Object Mosaic… Crear mosaico de objetos…
|
||||
Create Outlines Crear contornos
|
||||
Create Path Crear trazado
|
||||
Create PDF layers from top-level layers Crear capas PDF a partir de las capas de nivel superior
|
||||
Create PDF layers from layers and sublayers Crear capas PDF a partir de las capas y subcapas
|
||||
Create PDF-Compatible File Crear archivo compatible con PDF
|
||||
Create Report Crear informe
|
||||
Create Slice Crear sector
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -508,7 +508,7 @@
|
|||
Create Object Mosaic… Crea mosaico di oggetti…
|
||||
Create Outlines Crea contorni
|
||||
Create Path Crea tracciato
|
||||
Create PDF layers from top-level layers Crea livelli PDF dai livelli principali
|
||||
Create PDF layers from layers and sublayers Crea livelli PDF dai livelli e sottolivelli
|
||||
Create PDF-Compatible File Crea file compatibile PDF
|
||||
Create Report Crea rapporto
|
||||
Create Slice Crea sezione
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -983,7 +983,7 @@
|
|||
Create New Sublayer 新規サブレイヤーを作成
|
||||
Create New Swatch… 新規スウォッチを作成…
|
||||
Create New {kind} Style 新しい{kind}スタイルを作成
|
||||
Create PDF layers from top-level layers 最上位レイヤーから PDF レイヤーを作成
|
||||
Create PDF layers from layers and sublayers レイヤーとサブレイヤーから PDF レイヤーを作成
|
||||
Create PDF-Compatible File PDF 互換ファイルを作成
|
||||
Create Path パスを作成
|
||||
Create Slice スライスを作成
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -497,7 +497,7 @@
|
|||
Create New {kind} Style Criar Novo Estilo de {kind}
|
||||
Create Object Mosaic… Criar Mosaico de Objetos…
|
||||
Create Outlines Criar Contornos
|
||||
Create PDF layers from top-level layers Criar camadas de PDF a partir das camadas de nível superior
|
||||
Create PDF layers from layers and sublayers Criar camadas de PDF a partir das camadas e subcamadas
|
||||
Create PDF-Compatible File Criar Arquivo Compatível com PDF
|
||||
Create Path Criar Caminho
|
||||
Create Slice Criar Fatia
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -506,7 +506,7 @@
|
|||
Create Object Mosaic… Создать мозаику объектов…
|
||||
Create Outlines Создать контуры
|
||||
Create Path Создать контур
|
||||
Create PDF layers from top-level layers Создавать слои PDF из слоёв верхнего уровня
|
||||
Create PDF layers from layers and sublayers Создавать слои PDF из слоёв и подслоёв
|
||||
Create PDF-Compatible File Создать файл, совместимый с PDF
|
||||
Create Report Создать отчёт
|
||||
Create Slice Создать фрагмент
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -508,7 +508,7 @@
|
|||
Create Object Mosaic… 创建对象马赛克…
|
||||
Create Outlines 创建轮廓
|
||||
Create Path 创建路径
|
||||
Create PDF layers from top-level layers 从顶层图层创建 PDF 图层
|
||||
Create PDF layers from layers and sublayers 从图层和子图层创建 PDF 图层
|
||||
Create PDF-Compatible File 创建与 PDF 兼容的文件
|
||||
Create Report 创建报告
|
||||
Create Slice 创建切片
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
|
|
@ -523,7 +523,7 @@
|
|||
Create Object Mosaic… 建立物件馬賽克…
|
||||
Create Outlines 建立外框
|
||||
Create Path 建立路徑
|
||||
Create PDF layers from top-level layers 從最上層圖層建立 PDF 圖層
|
||||
Create PDF layers from layers and sublayers 從圖層和子圖層建立 PDF 圖層
|
||||
Create PDF-Compatible File 建立與 PDF 相容的檔案
|
||||
Create Report 建立報告
|
||||
Create Slice 建立切片
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
|
|
@ -355,9 +355,11 @@ A `security` password encrypts the file (RC4 40-bit at PDF 1.3, RC4 128-bit at 1
|
|||
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
|
||||
document's raster effects resolution, clipped the same way.
|
||||
`createLayers` writes each top-level layer (template layers are left out) as a PDF layer, an optional content group
|
||||
named as the layer: hidden layers are off, non-printing ones have `/PrintState /OFF` and locked ones are locked. It needs
|
||||
PDF 1.5 or later (at 1.4 it warns), and the file reopens with those layers.
|
||||
`createLayers` writes each layer and sublayer (template layers are left out) as a PDF layer, an optional content group
|
||||
named as the layer and listed under its parent layer's: hidden layers are off (their art is written, for the reader to
|
||||
show), non-printing ones have `/PrintState /OFF` and locked ones are locked. It needs PDF 1.5 or later (at 1.4 it
|
||||
warns), and the file reopens with those layers and sublayers. A `.ai` file writes them unless `createLayers` is false,
|
||||
so apps that read its PDF part find the layers, hidden ones included; hidden objects aren't in the PDF part.
|
||||
Images follow the `compression` settings of their kind (`color`, `gray`, or `mono` for black-and-white images): above
|
||||
`abovePpi` as placed they are resampled (`downsample`: `average`, `subsample` or `bicubic`) to `ppi`, and compressed
|
||||
with `zip`, `jpeg` (at `quality`; images with transparency stay lossless) or `auto` (JPEGs stay JPEG, the others
|
||||
|
|
|
|||
Loading…
Reference in New Issue