1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
use pdf_writer::types::MaskType;
use pdf_writer::{Chunk, Content, Filter, Finish, Ref};
use usvg::tiny_skia_path::PathSegment;
use usvg::{ClipPath, FillRule, Group, Node, Transform};
use super::group;
use super::path::draw_path;
use crate::util::context::Context;
use crate::util::helper::{
bbox_to_non_zero_rect, ContentExt, NameExt, RectExt, TransformExt,
};
use crate::util::resources::ResourceContainer;
use crate::Result;
/// Render a clip path into a content stream.
pub fn render(
group: &Group,
clip_path: &ClipPath,
chunk: &mut Chunk,
content: &mut Content,
ctx: &mut Context,
rc: &mut ResourceContainer,
) -> Result<()> {
// Unfortunately, clip paths are a bit tricky to deal with, the reason being that clip paths in
// SVGs can be much more complex than in PDF. In SVG, clip paths can have transforms, as well as
// nested clip paths. The objects inside of the clip path can have transforms as well, making it
// even more difficult to deal with. Because in PDF, once you start a clip path operation you
// cannot interrupt it, because once you pop the current graphics state, the clip path will be
// lost since it is part of the current graphics state. However, if we have various transforms
// on the children, we need to be able to push/pop the graphics state, so that the children's
// transforms don't affect each other. Initially, because of this, clip paths were only implemented
// using soft masks, but Safari has a couple of issues with rendering them properly. Not to mention
// the fact that soft masks are obviously also more expensive. Because of this, we proceed the following
// way: We first check whether the clip path itself is too "complex" (complex being that it fulfills
// some attributes that make it impossible to represent them in our current setup using just native
// PDF clip paths. If it is too complex, we fall back to using soft masks. However, if it is simple
// enough, we just use the normal clip path operator in PDF. It should be noted that in reality,
// only very few SVGs seem to have such complex clipping paths (they are not even rendered correctly
// by all online converters that were tested), so in most real-life scenarios, the simple version
// should suffice. But in order to conform with the SVG specification, we also handle the case
// of more complex clipping paths, even if this means that Safari will in some cases not
// display them correctly.
let is_simple_clip_path = is_simple_clip_path(clip_path.root());
let clip_rules = collect_clip_rules(clip_path.root());
if is_simple_clip_path
&& (clip_rules.iter().all(|f| *f == FillRule::NonZero)
// For even odd, there must be at most one shape in the group, because
// overlapping shapes with evenodd render differently in PDF
|| (clip_rules.iter().all(|f| *f == FillRule::EvenOdd)
&& clip_rules.len() == 1))
{
create_simple_clip_path(
clip_path,
content,
clip_rules.first().copied().unwrap_or(FillRule::NonZero),
);
} else {
let clip_path_ref = create_complex_clip_path(group, clip_path, chunk, ctx)?;
let clip_path_name = rc.add_graphics_state(clip_path_ref);
content.set_parameters(clip_path_name.to_pdf_name());
}
Ok(())
}
fn is_simple_clip_path(group: &Group) -> bool {
group.children().iter().all(|n| {
match n {
Node::Group(ref group) => {
// We can only intersect one clipping path with another one, meaning that we
// can convert nested clip paths if a second clip path is defined on the clip
// path itself, but not if it is defined on a child.
group.clip_path().is_none() && is_simple_clip_path(group)
}
_ => true,
}
})
}
fn collect_clip_rules(group: &Group) -> Vec<FillRule> {
let mut clip_rules = vec![];
group.children().iter().for_each(|n| match n {
Node::Path(ref path) => {
if let Some(fill) = &path.fill() {
clip_rules.push(fill.rule());
}
}
Node::Text(ref text) => clip_rules.extend(collect_clip_rules(text.flattened())),
Node::Group(ref group) => {
clip_rules.extend(collect_clip_rules(group));
}
_ => {}
});
clip_rules
}
fn create_simple_clip_path(
clip_path: &ClipPath,
content: &mut Content,
clip_rule: FillRule,
) {
if let Some(clip_path) = clip_path.clip_path() {
create_simple_clip_path(clip_path, content, clip_rule);
}
// Just a dummy operation, so that in case the clip path only has hidden children the clip
// path will still be applied and everything will be hidden.
content.move_to(0.0, 0.0);
let base_transform = clip_path.transform();
let mut segments = vec![];
extend_segments_from_group(clip_path.root(), &base_transform, &mut segments);
draw_path(segments.into_iter(), content);
if clip_rule == FillRule::NonZero {
content.clip_nonzero();
} else {
content.clip_even_odd();
}
content.end_path();
}
fn extend_segments_from_group(
group: &Group,
transform: &Transform,
segments: &mut Vec<PathSegment>,
) {
for child in group.children() {
match child {
Node::Path(ref path) => {
if path.is_visible() {
path.data().segments().for_each(|segment| match segment {
PathSegment::MoveTo(mut p) => {
transform.map_point(&mut p);
segments.push(PathSegment::MoveTo(p));
}
PathSegment::LineTo(mut p) => {
transform.map_point(&mut p);
segments.push(PathSegment::LineTo(p));
}
PathSegment::QuadTo(p1, p2) => {
let mut points = [p1, p2];
transform.map_points(&mut points);
segments.push(PathSegment::QuadTo(points[0], points[1]));
}
PathSegment::CubicTo(p1, p2, p3) => {
let mut points = [p1, p2, p3];
transform.map_points(&mut points);
segments.push(PathSegment::CubicTo(
points[0], points[1], points[2],
));
}
PathSegment::Close => segments.push(PathSegment::Close),
})
}
}
Node::Group(ref group) => {
let group_transform = transform.pre_concat(group.transform());
extend_segments_from_group(group, &group_transform, segments);
}
Node::Text(ref text) => {
// We could in theory preserve text in clip paths by using the appropriate
// rendering mode, but for now we just use the flattened version.
extend_segments_from_group(text.flattened(), transform, segments);
}
// Images are not valid in a clip path.
_ => {}
}
}
}
fn create_complex_clip_path(
parent: &Group,
clip_path: &ClipPath,
chunk: &mut Chunk,
ctx: &mut Context,
) -> Result<Ref> {
let mut rc = ResourceContainer::new();
let x_ref = ctx.alloc_ref();
let mut content = Content::new();
content.save_state_checked()?;
if let Some(clip_path) = clip_path.clip_path() {
render(parent, clip_path, chunk, &mut content, ctx, &mut rc)?;
}
content.transform(clip_path.transform().to_pdf_transform());
let pdf_bbox = bbox_to_non_zero_rect(Some(parent.bounding_box())).to_pdf_rect();
group::render(
clip_path.root(),
chunk,
&mut content,
ctx,
Transform::default(),
None,
&mut rc,
)?;
content.restore_state();
let content_stream = ctx.finish_content(content);
let mut x_object = chunk.form_xobject(x_ref, &content_stream);
if ctx.options.compress {
x_object.filter(Filter::FlateDecode);
}
rc.finish(&mut x_object.resources());
x_object
.group()
.transparency()
.isolated(false)
.knockout(false)
.color_space()
.icc_based(ctx.srgb_ref());
x_object.bbox(pdf_bbox);
x_object.finish();
let gs_ref = ctx.alloc_ref();
let mut gs = chunk.ext_graphics(gs_ref);
gs.soft_mask().subtype(MaskType::Alpha).group(x_ref);
Ok(gs_ref)
}