1 | // Copyright 2023 the Resvg Authors |
2 | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | |
4 | use std::fmt::Display; |
5 | use std::io::Write; |
6 | |
7 | use svgtypes::{parse_font_families, FontFamily}; |
8 | use xmlwriter::XmlWriter; |
9 | |
10 | use crate::parser::{AId, EId}; |
11 | use crate::*; |
12 | |
13 | impl Tree { |
14 | /// Writes `usvg::Tree` back to SVG. |
15 | pub fn to_string(&self, opt: &WriteOptions) -> String { |
16 | convert(self, opt) |
17 | } |
18 | } |
19 | |
20 | /// Checks that type has a default value. |
21 | trait IsDefault: Default { |
22 | /// Checks that type has a default value. |
23 | fn is_default(&self) -> bool; |
24 | } |
25 | |
26 | impl<T: Default + PartialEq + Copy> IsDefault for T { |
27 | #[inline ] |
28 | fn is_default(&self) -> bool { |
29 | *self == Self::default() |
30 | } |
31 | } |
32 | |
33 | /// XML writing options. |
34 | #[derive (Clone, Debug)] |
35 | pub struct WriteOptions { |
36 | /// Used to add a custom prefix to each element ID during writing. |
37 | pub id_prefix: Option<String>, |
38 | |
39 | /// Do not convert text into paths. |
40 | /// |
41 | /// Default: false |
42 | pub preserve_text: bool, |
43 | |
44 | /// Set the coordinates numeric precision. |
45 | /// |
46 | /// Smaller precision can lead to a malformed output in some cases. |
47 | /// |
48 | /// Default: 8 |
49 | pub coordinates_precision: u8, |
50 | |
51 | /// Set the transform values numeric precision. |
52 | /// |
53 | /// Smaller precision can lead to a malformed output in some cases. |
54 | /// |
55 | /// Default: 8 |
56 | pub transforms_precision: u8, |
57 | |
58 | /// Use single quote marks instead of double quote. |
59 | /// |
60 | /// # Examples |
61 | /// |
62 | /// Before: |
63 | /// |
64 | /// ```text |
65 | /// <rect fill="red"/> |
66 | /// ``` |
67 | /// |
68 | /// After: |
69 | /// |
70 | /// ```text |
71 | /// <rect fill='red'/> |
72 | /// ``` |
73 | /// |
74 | /// Default: disabled |
75 | pub use_single_quote: bool, |
76 | |
77 | /// Set XML nodes indention. |
78 | /// |
79 | /// # Examples |
80 | /// |
81 | /// `Indent::None` |
82 | /// Before: |
83 | /// |
84 | /// ```text |
85 | /// <svg> |
86 | /// <rect fill="red"/> |
87 | /// </svg> |
88 | /// ``` |
89 | /// |
90 | /// After: |
91 | /// |
92 | /// ```text |
93 | /// <svg><rect fill="red"/></svg> |
94 | /// ``` |
95 | /// |
96 | /// Default: 4 spaces |
97 | pub indent: Indent, |
98 | |
99 | /// Set XML attributes indention. |
100 | /// |
101 | /// # Examples |
102 | /// |
103 | /// `Indent::Spaces(2)` |
104 | /// |
105 | /// Before: |
106 | /// |
107 | /// ```text |
108 | /// <svg> |
109 | /// <rect fill="red" stroke="black"/> |
110 | /// </svg> |
111 | /// ``` |
112 | /// |
113 | /// After: |
114 | /// |
115 | /// ```text |
116 | /// <svg> |
117 | /// <rect |
118 | /// fill="red" |
119 | /// stroke="black"/> |
120 | /// </svg> |
121 | /// ``` |
122 | /// |
123 | /// Default: `None` |
124 | pub attributes_indent: Indent, |
125 | } |
126 | |
127 | impl Default for WriteOptions { |
128 | fn default() -> Self { |
129 | Self { |
130 | id_prefix: Default::default(), |
131 | preserve_text: false, |
132 | coordinates_precision: 8, |
133 | transforms_precision: 8, |
134 | use_single_quote: false, |
135 | indent: Indent::Spaces(4), |
136 | attributes_indent: Indent::None, |
137 | } |
138 | } |
139 | } |
140 | |
141 | pub(crate) fn convert(tree: &Tree, opt: &WriteOptions) -> String { |
142 | let mut xml: XmlWriter = XmlWriter::new(opt:xmlwriter::Options { |
143 | use_single_quote: opt.use_single_quote, |
144 | indent: opt.indent, |
145 | attributes_indent: opt.attributes_indent, |
146 | }); |
147 | |
148 | xml.start_svg_element(id:EId::Svg); |
149 | xml.write_svg_attribute(id:AId::Width, &tree.size.width()); |
150 | xml.write_svg_attribute(id:AId::Height, &tree.size.height()); |
151 | xml.write_attribute(name:"xmlns" , value:"http://www.w3.org/2000/svg" ); |
152 | if has_xlink(&tree.root) { |
153 | xml.write_attribute(name:"xmlns:xlink" , value:"http://www.w3.org/1999/xlink" ); |
154 | } |
155 | |
156 | xml.start_svg_element(id:EId::Defs); |
157 | write_defs(tree, opt, &mut xml); |
158 | xml.end_element(); |
159 | |
160 | write_elements(&tree.root, is_clip_path:false, opt, &mut xml); |
161 | |
162 | xml.end_document() |
163 | } |
164 | |
165 | fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { |
166 | let mut written_fe_image_nodes: Vec<String> = Vec::new(); |
167 | for filter in tree.filters() { |
168 | for fe in &filter.primitives { |
169 | if let filter::Kind::Image(ref img) = fe.kind { |
170 | if let Some(child) = img.root().children.first() { |
171 | if !written_fe_image_nodes.iter().any(|id| id == child.id()) { |
172 | write_element(child, false, opt, xml); |
173 | written_fe_image_nodes.push(child.id().to_string()); |
174 | } |
175 | } |
176 | } |
177 | } |
178 | |
179 | xml.start_svg_element(EId::Filter); |
180 | xml.write_id_attribute(filter.id(), opt); |
181 | xml.write_rect_attrs(filter.rect); |
182 | xml.write_units( |
183 | AId::FilterUnits, |
184 | Units::UserSpaceOnUse, |
185 | Units::ObjectBoundingBox, |
186 | ); |
187 | |
188 | for fe in &filter.primitives { |
189 | match fe.kind { |
190 | filter::Kind::DropShadow(ref shadow) => { |
191 | xml.start_svg_element(EId::FeDropShadow); |
192 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
193 | xml.write_filter_input(AId::In, &shadow.input); |
194 | xml.write_attribute_fmt( |
195 | AId::StdDeviation.to_str(), |
196 | format_args!(" {} {}" , shadow.std_dev_x.get(), shadow.std_dev_y.get()), |
197 | ); |
198 | xml.write_svg_attribute(AId::Dx, &shadow.dx); |
199 | xml.write_svg_attribute(AId::Dy, &shadow.dy); |
200 | xml.write_color(AId::FloodColor, shadow.color); |
201 | xml.write_svg_attribute(AId::FloodOpacity, &shadow.opacity.get()); |
202 | xml.write_svg_attribute(AId::Result, &fe.result); |
203 | xml.end_element(); |
204 | } |
205 | filter::Kind::GaussianBlur(ref blur) => { |
206 | xml.start_svg_element(EId::FeGaussianBlur); |
207 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
208 | xml.write_filter_input(AId::In, &blur.input); |
209 | xml.write_attribute_fmt( |
210 | AId::StdDeviation.to_str(), |
211 | format_args!(" {} {}" , blur.std_dev_x.get(), blur.std_dev_y.get()), |
212 | ); |
213 | xml.write_svg_attribute(AId::Result, &fe.result); |
214 | xml.end_element(); |
215 | } |
216 | filter::Kind::Offset(ref offset) => { |
217 | xml.start_svg_element(EId::FeOffset); |
218 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
219 | xml.write_filter_input(AId::In, &offset.input); |
220 | xml.write_svg_attribute(AId::Dx, &offset.dx); |
221 | xml.write_svg_attribute(AId::Dy, &offset.dy); |
222 | xml.write_svg_attribute(AId::Result, &fe.result); |
223 | xml.end_element(); |
224 | } |
225 | filter::Kind::Blend(ref blend) => { |
226 | xml.start_svg_element(EId::FeBlend); |
227 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
228 | xml.write_filter_input(AId::In, &blend.input1); |
229 | xml.write_filter_input(AId::In2, &blend.input2); |
230 | xml.write_svg_attribute( |
231 | AId::Mode, |
232 | match blend.mode { |
233 | BlendMode::Normal => "normal" , |
234 | BlendMode::Multiply => "multiply" , |
235 | BlendMode::Screen => "screen" , |
236 | BlendMode::Overlay => "overlay" , |
237 | BlendMode::Darken => "darken" , |
238 | BlendMode::Lighten => "lighten" , |
239 | BlendMode::ColorDodge => "color-dodge" , |
240 | BlendMode::ColorBurn => "color-burn" , |
241 | BlendMode::HardLight => "hard-light" , |
242 | BlendMode::SoftLight => "soft-light" , |
243 | BlendMode::Difference => "difference" , |
244 | BlendMode::Exclusion => "exclusion" , |
245 | BlendMode::Hue => "hue" , |
246 | BlendMode::Saturation => "saturation" , |
247 | BlendMode::Color => "color" , |
248 | BlendMode::Luminosity => "luminosity" , |
249 | }, |
250 | ); |
251 | xml.write_svg_attribute(AId::Result, &fe.result); |
252 | xml.end_element(); |
253 | } |
254 | filter::Kind::Flood(ref flood) => { |
255 | xml.start_svg_element(EId::FeFlood); |
256 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
257 | xml.write_color(AId::FloodColor, flood.color); |
258 | xml.write_svg_attribute(AId::FloodOpacity, &flood.opacity.get()); |
259 | xml.write_svg_attribute(AId::Result, &fe.result); |
260 | xml.end_element(); |
261 | } |
262 | filter::Kind::Composite(ref composite) => { |
263 | xml.start_svg_element(EId::FeComposite); |
264 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
265 | xml.write_filter_input(AId::In, &composite.input1); |
266 | xml.write_filter_input(AId::In2, &composite.input2); |
267 | xml.write_svg_attribute( |
268 | AId::Operator, |
269 | match composite.operator { |
270 | filter::CompositeOperator::Over => "over" , |
271 | filter::CompositeOperator::In => "in" , |
272 | filter::CompositeOperator::Out => "out" , |
273 | filter::CompositeOperator::Atop => "atop" , |
274 | filter::CompositeOperator::Xor => "xor" , |
275 | filter::CompositeOperator::Arithmetic { .. } => "arithmetic" , |
276 | }, |
277 | ); |
278 | |
279 | if let filter::CompositeOperator::Arithmetic { k1, k2, k3, k4 } = |
280 | composite.operator |
281 | { |
282 | xml.write_svg_attribute(AId::K1, &k1); |
283 | xml.write_svg_attribute(AId::K2, &k2); |
284 | xml.write_svg_attribute(AId::K3, &k3); |
285 | xml.write_svg_attribute(AId::K4, &k4); |
286 | } |
287 | |
288 | xml.write_svg_attribute(AId::Result, &fe.result); |
289 | xml.end_element(); |
290 | } |
291 | filter::Kind::Merge(ref merge) => { |
292 | xml.start_svg_element(EId::FeMerge); |
293 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
294 | xml.write_svg_attribute(AId::Result, &fe.result); |
295 | for input in &merge.inputs { |
296 | xml.start_svg_element(EId::FeMergeNode); |
297 | xml.write_filter_input(AId::In, input); |
298 | xml.end_element(); |
299 | } |
300 | |
301 | xml.end_element(); |
302 | } |
303 | filter::Kind::Tile(ref tile) => { |
304 | xml.start_svg_element(EId::FeTile); |
305 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
306 | xml.write_filter_input(AId::In, &tile.input); |
307 | xml.write_svg_attribute(AId::Result, &fe.result); |
308 | xml.end_element(); |
309 | } |
310 | filter::Kind::Image(ref img) => { |
311 | xml.start_svg_element(EId::FeImage); |
312 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
313 | if let Some(child) = img.root.children.first() { |
314 | let prefix = opt.id_prefix.as_deref().unwrap_or_default(); |
315 | xml.write_attribute_fmt( |
316 | "xlink:href" , |
317 | format_args!("# {}{}" , prefix, child.id()), |
318 | ); |
319 | } |
320 | |
321 | xml.write_svg_attribute(AId::Result, &fe.result); |
322 | xml.end_element(); |
323 | } |
324 | filter::Kind::ComponentTransfer(ref transfer) => { |
325 | xml.start_svg_element(EId::FeComponentTransfer); |
326 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
327 | xml.write_filter_input(AId::In, &transfer.input); |
328 | xml.write_svg_attribute(AId::Result, &fe.result); |
329 | |
330 | xml.write_filter_transfer_function(EId::FeFuncR, &transfer.func_r); |
331 | xml.write_filter_transfer_function(EId::FeFuncG, &transfer.func_g); |
332 | xml.write_filter_transfer_function(EId::FeFuncB, &transfer.func_b); |
333 | xml.write_filter_transfer_function(EId::FeFuncA, &transfer.func_a); |
334 | |
335 | xml.end_element(); |
336 | } |
337 | filter::Kind::ColorMatrix(ref matrix) => { |
338 | xml.start_svg_element(EId::FeColorMatrix); |
339 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
340 | xml.write_filter_input(AId::In, &matrix.input); |
341 | xml.write_svg_attribute(AId::Result, &fe.result); |
342 | |
343 | match matrix.kind { |
344 | filter::ColorMatrixKind::Matrix(ref values) => { |
345 | xml.write_svg_attribute(AId::Type, "matrix" ); |
346 | xml.write_numbers(AId::Values, values); |
347 | } |
348 | filter::ColorMatrixKind::Saturate(value) => { |
349 | xml.write_svg_attribute(AId::Type, "saturate" ); |
350 | xml.write_svg_attribute(AId::Values, &value.get()); |
351 | } |
352 | filter::ColorMatrixKind::HueRotate(angle) => { |
353 | xml.write_svg_attribute(AId::Type, "hueRotate" ); |
354 | xml.write_svg_attribute(AId::Values, &angle); |
355 | } |
356 | filter::ColorMatrixKind::LuminanceToAlpha => { |
357 | xml.write_svg_attribute(AId::Type, "luminanceToAlpha" ); |
358 | } |
359 | } |
360 | |
361 | xml.end_element(); |
362 | } |
363 | filter::Kind::ConvolveMatrix(ref matrix) => { |
364 | xml.start_svg_element(EId::FeConvolveMatrix); |
365 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
366 | xml.write_filter_input(AId::In, &matrix.input); |
367 | xml.write_svg_attribute(AId::Result, &fe.result); |
368 | |
369 | xml.write_attribute_fmt( |
370 | AId::Order.to_str(), |
371 | format_args!(" {} {}" , matrix.matrix.columns, matrix.matrix.rows), |
372 | ); |
373 | xml.write_numbers(AId::KernelMatrix, &matrix.matrix.data); |
374 | xml.write_svg_attribute(AId::Divisor, &matrix.divisor.get()); |
375 | xml.write_svg_attribute(AId::Bias, &matrix.bias); |
376 | xml.write_svg_attribute(AId::TargetX, &matrix.matrix.target_x); |
377 | xml.write_svg_attribute(AId::TargetY, &matrix.matrix.target_y); |
378 | xml.write_svg_attribute( |
379 | AId::EdgeMode, |
380 | match matrix.edge_mode { |
381 | filter::EdgeMode::None => "none" , |
382 | filter::EdgeMode::Duplicate => "duplicate" , |
383 | filter::EdgeMode::Wrap => "wrap" , |
384 | }, |
385 | ); |
386 | xml.write_svg_attribute( |
387 | AId::PreserveAlpha, |
388 | if matrix.preserve_alpha { |
389 | "true" |
390 | } else { |
391 | "false" |
392 | }, |
393 | ); |
394 | |
395 | xml.end_element(); |
396 | } |
397 | filter::Kind::Morphology(ref morphology) => { |
398 | xml.start_svg_element(EId::FeMorphology); |
399 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
400 | xml.write_filter_input(AId::In, &morphology.input); |
401 | xml.write_svg_attribute(AId::Result, &fe.result); |
402 | |
403 | xml.write_svg_attribute( |
404 | AId::Operator, |
405 | match morphology.operator { |
406 | filter::MorphologyOperator::Erode => "erode" , |
407 | filter::MorphologyOperator::Dilate => "dilate" , |
408 | }, |
409 | ); |
410 | xml.write_attribute_fmt( |
411 | AId::Radius.to_str(), |
412 | format_args!( |
413 | " {} {}" , |
414 | morphology.radius_x.get(), |
415 | morphology.radius_y.get() |
416 | ), |
417 | ); |
418 | |
419 | xml.end_element(); |
420 | } |
421 | filter::Kind::DisplacementMap(ref map) => { |
422 | xml.start_svg_element(EId::FeDisplacementMap); |
423 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
424 | xml.write_filter_input(AId::In, &map.input1); |
425 | xml.write_filter_input(AId::In2, &map.input2); |
426 | xml.write_svg_attribute(AId::Result, &fe.result); |
427 | |
428 | xml.write_svg_attribute(AId::Scale, &map.scale); |
429 | |
430 | let mut write_channel = |c, aid| { |
431 | xml.write_svg_attribute( |
432 | aid, |
433 | match c { |
434 | filter::ColorChannel::R => "R" , |
435 | filter::ColorChannel::G => "G" , |
436 | filter::ColorChannel::B => "B" , |
437 | filter::ColorChannel::A => "A" , |
438 | }, |
439 | ); |
440 | }; |
441 | write_channel(map.x_channel_selector, AId::XChannelSelector); |
442 | write_channel(map.y_channel_selector, AId::YChannelSelector); |
443 | |
444 | xml.end_element(); |
445 | } |
446 | filter::Kind::Turbulence(ref turbulence) => { |
447 | xml.start_svg_element(EId::FeTurbulence); |
448 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
449 | xml.write_svg_attribute(AId::Result, &fe.result); |
450 | |
451 | xml.write_attribute_fmt( |
452 | AId::BaseFrequency.to_str(), |
453 | format_args!( |
454 | " {} {}" , |
455 | turbulence.base_frequency_x.get(), |
456 | turbulence.base_frequency_y.get() |
457 | ), |
458 | ); |
459 | xml.write_svg_attribute(AId::NumOctaves, &turbulence.num_octaves); |
460 | xml.write_svg_attribute(AId::Seed, &turbulence.seed); |
461 | xml.write_svg_attribute( |
462 | AId::StitchTiles, |
463 | match turbulence.stitch_tiles { |
464 | true => "stitch" , |
465 | false => "noStitch" , |
466 | }, |
467 | ); |
468 | xml.write_svg_attribute( |
469 | AId::Type, |
470 | match turbulence.kind { |
471 | filter::TurbulenceKind::FractalNoise => "fractalNoise" , |
472 | filter::TurbulenceKind::Turbulence => "turbulence" , |
473 | }, |
474 | ); |
475 | |
476 | xml.end_element(); |
477 | } |
478 | filter::Kind::DiffuseLighting(ref light) => { |
479 | xml.start_svg_element(EId::FeDiffuseLighting); |
480 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
481 | xml.write_svg_attribute(AId::Result, &fe.result); |
482 | |
483 | xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); |
484 | xml.write_svg_attribute(AId::DiffuseConstant, &light.diffuse_constant); |
485 | xml.write_color(AId::LightingColor, light.lighting_color); |
486 | write_light_source(&light.light_source, xml); |
487 | |
488 | xml.end_element(); |
489 | } |
490 | filter::Kind::SpecularLighting(ref light) => { |
491 | xml.start_svg_element(EId::FeSpecularLighting); |
492 | xml.write_filter_primitive_attrs(filter.rect(), fe); |
493 | xml.write_svg_attribute(AId::Result, &fe.result); |
494 | |
495 | xml.write_svg_attribute(AId::SurfaceScale, &light.surface_scale); |
496 | xml.write_svg_attribute(AId::SpecularConstant, &light.specular_constant); |
497 | xml.write_svg_attribute(AId::SpecularExponent, &light.specular_exponent); |
498 | xml.write_color(AId::LightingColor, light.lighting_color); |
499 | write_light_source(&light.light_source, xml); |
500 | |
501 | xml.end_element(); |
502 | } |
503 | }; |
504 | } |
505 | |
506 | xml.end_element(); |
507 | } |
508 | } |
509 | |
510 | fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { |
511 | for lg in tree.linear_gradients() { |
512 | xml.start_svg_element(EId::LinearGradient); |
513 | xml.write_id_attribute(lg.id(), opt); |
514 | xml.write_svg_attribute(AId::X1, &lg.x1); |
515 | xml.write_svg_attribute(AId::Y1, &lg.y1); |
516 | xml.write_svg_attribute(AId::X2, &lg.x2); |
517 | xml.write_svg_attribute(AId::Y2, &lg.y2); |
518 | write_base_grad(&lg.base, opt, xml); |
519 | xml.end_element(); |
520 | } |
521 | |
522 | for rg in tree.radial_gradients() { |
523 | xml.start_svg_element(EId::RadialGradient); |
524 | xml.write_id_attribute(rg.id(), opt); |
525 | xml.write_svg_attribute(AId::Cx, &rg.cx); |
526 | xml.write_svg_attribute(AId::Cy, &rg.cy); |
527 | xml.write_svg_attribute(AId::R, &rg.r.get()); |
528 | xml.write_svg_attribute(AId::Fx, &rg.fx); |
529 | xml.write_svg_attribute(AId::Fy, &rg.fy); |
530 | write_base_grad(&rg.base, opt, xml); |
531 | xml.end_element(); |
532 | } |
533 | |
534 | for pattern in tree.patterns() { |
535 | xml.start_svg_element(EId::Pattern); |
536 | xml.write_id_attribute(pattern.id(), opt); |
537 | xml.write_rect_attrs(pattern.rect); |
538 | xml.write_units(AId::PatternUnits, pattern.units, Units::ObjectBoundingBox); |
539 | xml.write_units( |
540 | AId::PatternContentUnits, |
541 | pattern.content_units, |
542 | Units::UserSpaceOnUse, |
543 | ); |
544 | xml.write_transform(AId::PatternTransform, pattern.transform, opt); |
545 | |
546 | write_elements(&pattern.root, false, opt, xml); |
547 | |
548 | xml.end_element(); |
549 | } |
550 | |
551 | if tree.has_text_nodes() { |
552 | write_text_path_paths(&tree.root, opt, xml); |
553 | } |
554 | |
555 | write_filters(tree, opt, xml); |
556 | |
557 | for clip in tree.clip_paths() { |
558 | xml.start_svg_element(EId::ClipPath); |
559 | xml.write_id_attribute(clip.id(), opt); |
560 | xml.write_transform(AId::Transform, clip.transform, opt); |
561 | |
562 | if let Some(ref clip) = clip.clip_path { |
563 | xml.write_func_iri(AId::ClipPath, clip.id(), opt); |
564 | } |
565 | |
566 | write_elements(&clip.root, true, opt, xml); |
567 | |
568 | xml.end_element(); |
569 | } |
570 | |
571 | for mask in tree.masks() { |
572 | xml.start_svg_element(EId::Mask); |
573 | xml.write_id_attribute(mask.id(), opt); |
574 | if mask.kind == MaskType::Alpha { |
575 | xml.write_svg_attribute(AId::MaskType, "alpha" ); |
576 | } |
577 | xml.write_units( |
578 | AId::MaskUnits, |
579 | Units::UserSpaceOnUse, |
580 | Units::ObjectBoundingBox, |
581 | ); |
582 | xml.write_rect_attrs(mask.rect); |
583 | |
584 | if let Some(ref mask) = mask.mask { |
585 | xml.write_func_iri(AId::Mask, mask.id(), opt); |
586 | } |
587 | |
588 | write_elements(&mask.root, false, opt, xml); |
589 | |
590 | xml.end_element(); |
591 | } |
592 | } |
593 | |
594 | fn write_text_path_paths(parent: &Group, opt: &WriteOptions, xml: &mut XmlWriter) { |
595 | for node in &parent.children { |
596 | if let Node::Group(ref group) = node { |
597 | write_text_path_paths(group, opt, xml); |
598 | } else if let Node::Text(ref text) = node { |
599 | for chunk in &text.chunks { |
600 | if let TextFlow::Path(ref text_path) = chunk.text_flow { |
601 | let path = Path::new( |
602 | text_path.id().to_string(), |
603 | true, |
604 | None, |
605 | None, |
606 | PaintOrder::default(), |
607 | ShapeRendering::default(), |
608 | text_path.path.clone(), |
609 | Transform::default(), |
610 | ); |
611 | if let Some(ref path) = path { |
612 | write_path(path, false, Transform::default(), None, opt, xml); |
613 | } |
614 | } |
615 | } |
616 | } |
617 | |
618 | node.subroots(|subroot| write_text_path_paths(subroot, opt, xml)); |
619 | } |
620 | } |
621 | |
622 | fn write_elements(parent: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { |
623 | for n: &Node in &parent.children { |
624 | write_element(node:n, is_clip_path, opt, xml); |
625 | } |
626 | } |
627 | |
628 | fn write_element(node: &Node, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { |
629 | match node { |
630 | Node::Path(ref p) => { |
631 | write_path(p, is_clip_path, Transform::default(), None, opt, xml); |
632 | } |
633 | Node::Image(ref img) => { |
634 | xml.start_svg_element(EId::Image); |
635 | if !img.id.is_empty() { |
636 | xml.write_id_attribute(&img.id, opt); |
637 | } |
638 | |
639 | xml.write_svg_attribute(AId::Width, &img.size().width()); |
640 | xml.write_svg_attribute(AId::Height, &img.size().height()); |
641 | |
642 | xml.write_visibility(img.visible); |
643 | |
644 | match img.rendering_mode { |
645 | ImageRendering::OptimizeQuality => {} |
646 | ImageRendering::OptimizeSpeed => { |
647 | xml.write_svg_attribute(AId::ImageRendering, "optimizeSpeed" ); |
648 | } |
649 | ImageRendering::Smooth => { |
650 | xml.write_attribute(AId::Style.to_str(), "image-rendering:smooth" ); |
651 | } |
652 | ImageRendering::HighQuality => { |
653 | xml.write_attribute(AId::Style.to_str(), "image-rendering:high-quality" ); |
654 | } |
655 | ImageRendering::CrispEdges => { |
656 | xml.write_attribute(AId::Style.to_str(), "image-rendering:crisp-edges" ); |
657 | } |
658 | ImageRendering::Pixelated => { |
659 | xml.write_attribute(AId::Style.to_str(), "image-rendering:pixelated" ); |
660 | } |
661 | } |
662 | |
663 | xml.write_image_data(&img.kind); |
664 | |
665 | xml.end_element(); |
666 | } |
667 | Node::Group(ref g) => { |
668 | write_group_element(g, is_clip_path, opt, xml); |
669 | } |
670 | Node::Text(ref text) => { |
671 | if opt.preserve_text { |
672 | xml.start_svg_element(EId::Text); |
673 | |
674 | if !text.id.is_empty() { |
675 | xml.write_id_attribute(&text.id, opt); |
676 | } |
677 | |
678 | xml.write_attribute("xml:space" , "preserve" ); |
679 | |
680 | match text.writing_mode { |
681 | WritingMode::LeftToRight => {} |
682 | WritingMode::TopToBottom => xml.write_svg_attribute(AId::WritingMode, "tb" ), |
683 | } |
684 | |
685 | match text.rendering_mode { |
686 | TextRendering::OptimizeSpeed => { |
687 | xml.write_svg_attribute(AId::TextRendering, "optimizeSpeed" ) |
688 | } |
689 | TextRendering::GeometricPrecision => { |
690 | xml.write_svg_attribute(AId::TextRendering, "geometricPrecision" ) |
691 | } |
692 | TextRendering::OptimizeLegibility => {} |
693 | } |
694 | |
695 | if text.rotate.iter().any(|r| *r != 0.0) { |
696 | xml.write_numbers(AId::Rotate, &text.rotate); |
697 | } |
698 | |
699 | if text.dx.iter().any(|dx| *dx != 0.0) { |
700 | xml.write_numbers(AId::Dx, &text.dx); |
701 | } |
702 | |
703 | if text.dy.iter().any(|dy| *dy != 0.0) { |
704 | xml.write_numbers(AId::Dy, &text.dy); |
705 | } |
706 | |
707 | xml.set_preserve_whitespaces(true); |
708 | |
709 | for chunk in &text.chunks { |
710 | if let TextFlow::Path(text_path) = &chunk.text_flow { |
711 | xml.start_svg_element(EId::TextPath); |
712 | |
713 | let prefix = opt.id_prefix.as_deref().unwrap_or_default(); |
714 | xml.write_attribute_fmt( |
715 | "xlink:href" , |
716 | format_args!("# {}{}" , prefix, text_path.id()), |
717 | ); |
718 | |
719 | if text_path.start_offset != 0.0 { |
720 | xml.write_svg_attribute(AId::StartOffset, &text_path.start_offset); |
721 | } |
722 | } |
723 | |
724 | xml.start_svg_element(EId::Tspan); |
725 | |
726 | if let Some(x) = chunk.x { |
727 | xml.write_svg_attribute(AId::X, &x); |
728 | } |
729 | |
730 | if let Some(y) = chunk.y { |
731 | xml.write_svg_attribute(AId::Y, &y); |
732 | } |
733 | |
734 | match chunk.anchor { |
735 | TextAnchor::Start => {} |
736 | TextAnchor::Middle => xml.write_svg_attribute(AId::TextAnchor, "middle" ), |
737 | TextAnchor::End => xml.write_svg_attribute(AId::TextAnchor, "end" ), |
738 | } |
739 | |
740 | for span in &chunk.spans { |
741 | let decorations: Vec<_> = [ |
742 | ("underline" , &span.decoration.underline), |
743 | ("line-through" , &span.decoration.line_through), |
744 | ("overline" , &span.decoration.overline), |
745 | ] |
746 | .iter() |
747 | .filter_map(|&(key, option_value)| { |
748 | option_value.as_ref().map(|value| (key, value)) |
749 | }) |
750 | .collect(); |
751 | |
752 | // Decorations need to be dumped BEFORE we write the actual span data |
753 | // (so that for example stroke color of span doesn't affect the text |
754 | // itself while baseline shifts need to be written after (since they are |
755 | // affected by the font size) |
756 | for (deco_name, deco) in &decorations { |
757 | xml.start_svg_element(EId::Tspan); |
758 | xml.write_svg_attribute(AId::TextDecoration, deco_name); |
759 | write_fill(&deco.fill, false, opt, xml); |
760 | write_stroke(&deco.stroke, opt, xml); |
761 | } |
762 | |
763 | write_span(is_clip_path, opt, xml, chunk, span); |
764 | |
765 | // End for each tspan we needed to create for decorations |
766 | for _ in &decorations { |
767 | xml.end_element(); |
768 | } |
769 | } |
770 | xml.end_element(); |
771 | |
772 | // End textPath element |
773 | if matches!(&chunk.text_flow, TextFlow::Path(_)) { |
774 | xml.end_element(); |
775 | } |
776 | } |
777 | |
778 | xml.end_element(); |
779 | xml.set_preserve_whitespaces(false); |
780 | } else { |
781 | write_group_element(text.flattened(), is_clip_path, opt, xml); |
782 | } |
783 | } |
784 | } |
785 | } |
786 | |
787 | fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { |
788 | if is_clip_path { |
789 | // The `clipPath` element in SVG doesn't allow groups, only shapes and text. |
790 | // The problem is that in `usvg` we can set a `clip-path` only on groups. |
791 | // So in cases when a `clipPath` child has a `clip-path` as well, |
792 | // it would be inside a group. And we have to skip this group during writing. |
793 | // |
794 | // Basically, the following SVG: |
795 | // |
796 | // <clipPath id="clip"> |
797 | // <path clip-path="url(#clip-nested)"/> |
798 | // </clipPath> |
799 | // |
800 | // will be represented in usvg as: |
801 | // |
802 | // <clipPath id="clip"> |
803 | // <g clip-path="url(#clip-nested)"> |
804 | // <path/> |
805 | // </g> |
806 | // </clipPath> |
807 | // |
808 | // |
809 | // Same with text. Text elements will be converted into groups, |
810 | // but only the group's children should be written. |
811 | for child in &g.children { |
812 | if let Node::Path(ref path) = child { |
813 | let clip_id = g.clip_path.as_ref().map(|cp| cp.id().to_string()); |
814 | write_path( |
815 | path, |
816 | is_clip_path, |
817 | g.transform, |
818 | clip_id.as_deref(), |
819 | opt, |
820 | xml, |
821 | ); |
822 | } |
823 | } |
824 | return; |
825 | } |
826 | |
827 | xml.start_svg_element(EId::G); |
828 | if !g.id.is_empty() { |
829 | xml.write_id_attribute(&g.id, opt); |
830 | }; |
831 | |
832 | if let Some(ref clip) = g.clip_path { |
833 | xml.write_func_iri(AId::ClipPath, clip.id(), opt); |
834 | } |
835 | |
836 | if let Some(ref mask) = g.mask { |
837 | xml.write_func_iri(AId::Mask, mask.id(), opt); |
838 | } |
839 | |
840 | if !g.filters.is_empty() { |
841 | let prefix = opt.id_prefix.as_deref().unwrap_or_default(); |
842 | let ids: Vec<_> = g |
843 | .filters |
844 | .iter() |
845 | .map(|filter| format!("url(# {}{})" , prefix, filter.id())) |
846 | .collect(); |
847 | xml.write_svg_attribute(AId::Filter, &ids.join(" " )); |
848 | } |
849 | |
850 | if g.opacity != Opacity::ONE { |
851 | xml.write_svg_attribute(AId::Opacity, &g.opacity.get()); |
852 | } |
853 | |
854 | xml.write_transform(AId::Transform, g.transform, opt); |
855 | |
856 | if g.blend_mode != BlendMode::Normal || g.isolate { |
857 | let blend_mode = match g.blend_mode { |
858 | BlendMode::Normal => "normal" , |
859 | BlendMode::Multiply => "multiply" , |
860 | BlendMode::Screen => "screen" , |
861 | BlendMode::Overlay => "overlay" , |
862 | BlendMode::Darken => "darken" , |
863 | BlendMode::Lighten => "lighten" , |
864 | BlendMode::ColorDodge => "color-dodge" , |
865 | BlendMode::ColorBurn => "color-burn" , |
866 | BlendMode::HardLight => "hard-light" , |
867 | BlendMode::SoftLight => "soft-light" , |
868 | BlendMode::Difference => "difference" , |
869 | BlendMode::Exclusion => "exclusion" , |
870 | BlendMode::Hue => "hue" , |
871 | BlendMode::Saturation => "saturation" , |
872 | BlendMode::Color => "color" , |
873 | BlendMode::Luminosity => "luminosity" , |
874 | }; |
875 | |
876 | // For reasons unknown, `mix-blend-mode` and `isolation` must be written |
877 | // as `style` attribute. |
878 | let isolation = if g.isolate { "isolate" } else { "auto" }; |
879 | xml.write_attribute_fmt( |
880 | AId::Style.to_str(), |
881 | format_args!("mix-blend-mode: {};isolation: {}" , blend_mode, isolation), |
882 | ); |
883 | } |
884 | |
885 | write_elements(g, false, opt, xml); |
886 | |
887 | xml.end_element(); |
888 | } |
889 | |
890 | trait XmlWriterExt { |
891 | fn start_svg_element(&mut self, id: EId); |
892 | fn write_svg_attribute<V: Display + ?Sized>(&mut self, id: AId, value: &V); |
893 | fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions); |
894 | fn write_color(&mut self, id: AId, color: Color); |
895 | fn write_units(&mut self, id: AId, units: Units, def: Units); |
896 | fn write_transform(&mut self, id: AId, units: Transform, opt: &WriteOptions); |
897 | fn write_visibility(&mut self, value: bool); |
898 | fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions); |
899 | fn write_rect_attrs(&mut self, r: NonZeroRect); |
900 | fn write_numbers(&mut self, aid: AId, list: &[f32]); |
901 | fn write_image_data(&mut self, kind: &ImageKind); |
902 | fn write_filter_input(&mut self, id: AId, input: &filter::Input); |
903 | fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive); |
904 | fn write_filter_transfer_function(&mut self, eid: EId, fe: &filter::TransferFunction); |
905 | } |
906 | |
907 | impl XmlWriterExt for XmlWriter { |
908 | #[inline (never)] |
909 | fn start_svg_element(&mut self, id: EId) { |
910 | self.start_element(id.to_str()); |
911 | } |
912 | |
913 | #[inline (never)] |
914 | fn write_svg_attribute<V: Display + ?Sized>(&mut self, id: AId, value: &V) { |
915 | self.write_attribute(id.to_str(), value) |
916 | } |
917 | |
918 | #[inline (never)] |
919 | fn write_id_attribute(&mut self, id: &str, opt: &WriteOptions) { |
920 | debug_assert!(!id.is_empty()); |
921 | |
922 | if let Some(ref prefix) = opt.id_prefix { |
923 | let full_id = format!(" {}{}" , prefix, id); |
924 | self.write_attribute("id" , &full_id); |
925 | } else { |
926 | self.write_attribute("id" , id); |
927 | } |
928 | } |
929 | |
930 | #[inline (never)] |
931 | fn write_color(&mut self, id: AId, c: Color) { |
932 | static CHARS: &[u8] = b"0123456789abcdef" ; |
933 | |
934 | #[inline ] |
935 | fn int2hex(n: u8) -> (u8, u8) { |
936 | (CHARS[(n >> 4) as usize], CHARS[(n & 0xf) as usize]) |
937 | } |
938 | |
939 | let (r1, r2) = int2hex(c.red); |
940 | let (g1, g2) = int2hex(c.green); |
941 | let (b1, b2) = int2hex(c.blue); |
942 | |
943 | self.write_attribute_raw(id.to_str(), |buf| { |
944 | buf.extend_from_slice(&[b'#' , r1, r2, g1, g2, b1, b2]) |
945 | }); |
946 | } |
947 | |
948 | // TODO: simplify |
949 | fn write_units(&mut self, id: AId, units: Units, def: Units) { |
950 | if units != def { |
951 | self.write_attribute( |
952 | id.to_str(), |
953 | match units { |
954 | Units::UserSpaceOnUse => "userSpaceOnUse" , |
955 | Units::ObjectBoundingBox => "objectBoundingBox" , |
956 | }, |
957 | ); |
958 | } |
959 | } |
960 | |
961 | fn write_transform(&mut self, id: AId, ts: Transform, opt: &WriteOptions) { |
962 | if !ts.is_default() { |
963 | self.write_attribute_raw(id.to_str(), |buf| { |
964 | buf.extend_from_slice(b"matrix(" ); |
965 | write_num(ts.sx, buf, opt.transforms_precision); |
966 | buf.push(b' ' ); |
967 | write_num(ts.ky, buf, opt.transforms_precision); |
968 | buf.push(b' ' ); |
969 | write_num(ts.kx, buf, opt.transforms_precision); |
970 | buf.push(b' ' ); |
971 | write_num(ts.sy, buf, opt.transforms_precision); |
972 | buf.push(b' ' ); |
973 | write_num(ts.tx, buf, opt.transforms_precision); |
974 | buf.push(b' ' ); |
975 | write_num(ts.ty, buf, opt.transforms_precision); |
976 | buf.extend_from_slice(b")" ); |
977 | }); |
978 | } |
979 | } |
980 | |
981 | fn write_visibility(&mut self, value: bool) { |
982 | if !value { |
983 | self.write_attribute(AId::Visibility.to_str(), "hidden" ); |
984 | } |
985 | } |
986 | |
987 | fn write_func_iri(&mut self, aid: AId, id: &str, opt: &WriteOptions) { |
988 | debug_assert!(!id.is_empty()); |
989 | let prefix = opt.id_prefix.as_deref().unwrap_or_default(); |
990 | self.write_attribute_fmt(aid.to_str(), format_args!("url(# {}{})" , prefix, id)); |
991 | } |
992 | |
993 | fn write_rect_attrs(&mut self, r: NonZeroRect) { |
994 | self.write_svg_attribute(AId::X, &r.x()); |
995 | self.write_svg_attribute(AId::Y, &r.y()); |
996 | self.write_svg_attribute(AId::Width, &r.width()); |
997 | self.write_svg_attribute(AId::Height, &r.height()); |
998 | } |
999 | |
1000 | fn write_numbers(&mut self, aid: AId, list: &[f32]) { |
1001 | self.write_attribute_raw(aid.to_str(), |buf| { |
1002 | for n in list { |
1003 | buf.write_fmt(format_args!(" {} " , n)).unwrap(); |
1004 | } |
1005 | |
1006 | if !list.is_empty() { |
1007 | buf.pop(); |
1008 | } |
1009 | }); |
1010 | } |
1011 | |
1012 | fn write_filter_input(&mut self, id: AId, input: &filter::Input) { |
1013 | self.write_attribute( |
1014 | id.to_str(), |
1015 | match input { |
1016 | filter::Input::SourceGraphic => "SourceGraphic" , |
1017 | filter::Input::SourceAlpha => "SourceAlpha" , |
1018 | filter::Input::Reference(ref s) => s, |
1019 | }, |
1020 | ); |
1021 | } |
1022 | |
1023 | fn write_filter_primitive_attrs(&mut self, parent_rect: NonZeroRect, fe: &filter::Primitive) { |
1024 | if parent_rect.x() != fe.rect().x() { |
1025 | self.write_svg_attribute(AId::X, &fe.rect().x()); |
1026 | } |
1027 | if parent_rect.y() != fe.rect().y() { |
1028 | self.write_svg_attribute(AId::Y, &fe.rect().y()); |
1029 | } |
1030 | if parent_rect.width() != fe.rect().width() { |
1031 | self.write_svg_attribute(AId::Width, &fe.rect().width()); |
1032 | } |
1033 | if parent_rect.height() != fe.rect().height() { |
1034 | self.write_svg_attribute(AId::Height, &fe.rect().height()); |
1035 | } |
1036 | |
1037 | self.write_attribute( |
1038 | AId::ColorInterpolationFilters.to_str(), |
1039 | match fe.color_interpolation { |
1040 | filter::ColorInterpolation::SRGB => "sRGB" , |
1041 | filter::ColorInterpolation::LinearRGB => "linearRGB" , |
1042 | }, |
1043 | ); |
1044 | } |
1045 | |
1046 | fn write_filter_transfer_function(&mut self, eid: EId, fe: &filter::TransferFunction) { |
1047 | self.start_svg_element(eid); |
1048 | |
1049 | match fe { |
1050 | filter::TransferFunction::Identity => { |
1051 | self.write_svg_attribute(AId::Type, "identity" ); |
1052 | } |
1053 | filter::TransferFunction::Table(ref values) => { |
1054 | self.write_svg_attribute(AId::Type, "table" ); |
1055 | self.write_numbers(AId::TableValues, values); |
1056 | } |
1057 | filter::TransferFunction::Discrete(ref values) => { |
1058 | self.write_svg_attribute(AId::Type, "discrete" ); |
1059 | self.write_numbers(AId::TableValues, values); |
1060 | } |
1061 | filter::TransferFunction::Linear { slope, intercept } => { |
1062 | self.write_svg_attribute(AId::Type, "linear" ); |
1063 | self.write_svg_attribute(AId::Slope, &slope); |
1064 | self.write_svg_attribute(AId::Intercept, &intercept); |
1065 | } |
1066 | filter::TransferFunction::Gamma { |
1067 | amplitude, |
1068 | exponent, |
1069 | offset, |
1070 | } => { |
1071 | self.write_svg_attribute(AId::Type, "gamma" ); |
1072 | self.write_svg_attribute(AId::Amplitude, &litude); |
1073 | self.write_svg_attribute(AId::Exponent, &exponent); |
1074 | self.write_svg_attribute(AId::Offset, &offset); |
1075 | } |
1076 | } |
1077 | |
1078 | self.end_element(); |
1079 | } |
1080 | |
1081 | fn write_image_data(&mut self, kind: &ImageKind) { |
1082 | let svg_string; |
1083 | let (mime, data) = match kind { |
1084 | ImageKind::JPEG(ref data) => ("jpeg" , data.as_slice()), |
1085 | ImageKind::PNG(ref data) => ("png" , data.as_slice()), |
1086 | ImageKind::GIF(ref data) => ("gif" , data.as_slice()), |
1087 | ImageKind::WEBP(ref data) => ("webp" , data.as_slice()), |
1088 | ImageKind::SVG(ref tree) => { |
1089 | svg_string = tree.to_string(&WriteOptions::default()); |
1090 | ("svg+xml" , svg_string.as_bytes()) |
1091 | } |
1092 | }; |
1093 | |
1094 | self.write_attribute_raw("xlink:href" , |buf| { |
1095 | buf.extend_from_slice(b"data:image/" ); |
1096 | buf.extend_from_slice(mime.as_bytes()); |
1097 | buf.extend_from_slice(b";base64, " ); |
1098 | |
1099 | let mut enc = |
1100 | base64::write::EncoderWriter::new(buf, &base64::engine::general_purpose::STANDARD); |
1101 | enc.write_all(data).unwrap(); |
1102 | enc.finish().unwrap(); |
1103 | }); |
1104 | } |
1105 | } |
1106 | |
1107 | fn has_xlink(parent: &Group) -> bool { |
1108 | for node in &parent.children { |
1109 | match node { |
1110 | Node::Group(ref g) => { |
1111 | for filter in &g.filters { |
1112 | if filter |
1113 | .primitives |
1114 | .iter() |
1115 | .any(|p| matches!(p.kind, filter::Kind::Image(_))) |
1116 | { |
1117 | return true; |
1118 | } |
1119 | } |
1120 | |
1121 | if let Some(ref mask) = g.mask { |
1122 | if has_xlink(mask.root()) { |
1123 | return true; |
1124 | } |
1125 | |
1126 | if let Some(ref sub_mask) = mask.mask { |
1127 | if has_xlink(&sub_mask.root) { |
1128 | return true; |
1129 | } |
1130 | } |
1131 | } |
1132 | |
1133 | if has_xlink(g) { |
1134 | return true; |
1135 | } |
1136 | } |
1137 | Node::Image(_) => { |
1138 | return true; |
1139 | } |
1140 | Node::Text(ref text) => { |
1141 | if text |
1142 | .chunks |
1143 | .iter() |
1144 | .any(|t| matches!(t.text_flow, TextFlow::Path(_))) |
1145 | { |
1146 | return true; |
1147 | } |
1148 | } |
1149 | _ => {} |
1150 | } |
1151 | |
1152 | let mut present = false; |
1153 | node.subroots(|root| present |= has_xlink(root)); |
1154 | if present { |
1155 | return true; |
1156 | } |
1157 | } |
1158 | |
1159 | false |
1160 | } |
1161 | |
1162 | fn write_base_grad(g: &BaseGradient, opt: &WriteOptions, xml: &mut XmlWriter) { |
1163 | xml.write_units(id:AId::GradientUnits, g.units, def:Units::ObjectBoundingBox); |
1164 | xml.write_transform(id:AId::GradientTransform, units:g.transform, opt); |
1165 | |
1166 | match g.spread_method { |
1167 | SpreadMethod::Pad => {} |
1168 | SpreadMethod::Reflect => xml.write_svg_attribute(id:AId::SpreadMethod, value:"reflect" ), |
1169 | SpreadMethod::Repeat => xml.write_svg_attribute(id:AId::SpreadMethod, value:"repeat" ), |
1170 | } |
1171 | |
1172 | for s: &Stop in &g.stops { |
1173 | xml.start_svg_element(id:EId::Stop); |
1174 | xml.write_svg_attribute(id:AId::Offset, &s.offset.get()); |
1175 | xml.write_color(id:AId::StopColor, s.color); |
1176 | if s.opacity != Opacity::ONE { |
1177 | xml.write_svg_attribute(id:AId::StopOpacity, &s.opacity.get()); |
1178 | } |
1179 | |
1180 | xml.end_element(); |
1181 | } |
1182 | } |
1183 | |
1184 | fn write_path( |
1185 | path: &Path, |
1186 | is_clip_path: bool, |
1187 | path_transform: Transform, |
1188 | clip_path: Option<&str>, |
1189 | opt: &WriteOptions, |
1190 | xml: &mut XmlWriter, |
1191 | ) { |
1192 | xml.start_svg_element(EId::Path); |
1193 | if !path.id.is_empty() { |
1194 | xml.write_id_attribute(&path.id, opt); |
1195 | } |
1196 | |
1197 | write_fill(&path.fill, is_clip_path, opt, xml); |
1198 | write_stroke(&path.stroke, opt, xml); |
1199 | |
1200 | xml.write_visibility(path.visible); |
1201 | |
1202 | if path.paint_order == PaintOrder::StrokeAndFill { |
1203 | xml.write_svg_attribute(AId::PaintOrder, "stroke" ); |
1204 | } |
1205 | |
1206 | match path.rendering_mode { |
1207 | ShapeRendering::OptimizeSpeed => { |
1208 | xml.write_svg_attribute(AId::ShapeRendering, "optimizeSpeed" ); |
1209 | } |
1210 | ShapeRendering::CrispEdges => xml.write_svg_attribute(AId::ShapeRendering, "crispEdges" ), |
1211 | ShapeRendering::GeometricPrecision => {} |
1212 | } |
1213 | |
1214 | if let Some(id) = clip_path { |
1215 | xml.write_func_iri(AId::ClipPath, id, opt); |
1216 | } |
1217 | |
1218 | xml.write_transform(AId::Transform, path_transform, opt); |
1219 | |
1220 | xml.write_attribute_raw("d" , |buf| { |
1221 | use tiny_skia_path::PathSegment; |
1222 | |
1223 | for seg in path.data.segments() { |
1224 | match seg { |
1225 | PathSegment::MoveTo(p) => { |
1226 | buf.extend_from_slice(b"M " ); |
1227 | write_num(p.x, buf, opt.coordinates_precision); |
1228 | buf.push(b' ' ); |
1229 | write_num(p.y, buf, opt.coordinates_precision); |
1230 | buf.push(b' ' ); |
1231 | } |
1232 | PathSegment::LineTo(p) => { |
1233 | buf.extend_from_slice(b"L " ); |
1234 | write_num(p.x, buf, opt.coordinates_precision); |
1235 | buf.push(b' ' ); |
1236 | write_num(p.y, buf, opt.coordinates_precision); |
1237 | buf.push(b' ' ); |
1238 | } |
1239 | PathSegment::QuadTo(p1, p) => { |
1240 | buf.extend_from_slice(b"Q " ); |
1241 | write_num(p1.x, buf, opt.coordinates_precision); |
1242 | buf.push(b' ' ); |
1243 | write_num(p1.y, buf, opt.coordinates_precision); |
1244 | buf.push(b' ' ); |
1245 | write_num(p.x, buf, opt.coordinates_precision); |
1246 | buf.push(b' ' ); |
1247 | write_num(p.y, buf, opt.coordinates_precision); |
1248 | buf.push(b' ' ); |
1249 | } |
1250 | PathSegment::CubicTo(p1, p2, p) => { |
1251 | buf.extend_from_slice(b"C " ); |
1252 | write_num(p1.x, buf, opt.coordinates_precision); |
1253 | buf.push(b' ' ); |
1254 | write_num(p1.y, buf, opt.coordinates_precision); |
1255 | buf.push(b' ' ); |
1256 | write_num(p2.x, buf, opt.coordinates_precision); |
1257 | buf.push(b' ' ); |
1258 | write_num(p2.y, buf, opt.coordinates_precision); |
1259 | buf.push(b' ' ); |
1260 | write_num(p.x, buf, opt.coordinates_precision); |
1261 | buf.push(b' ' ); |
1262 | write_num(p.y, buf, opt.coordinates_precision); |
1263 | buf.push(b' ' ); |
1264 | } |
1265 | PathSegment::Close => { |
1266 | buf.extend_from_slice(b"Z " ); |
1267 | } |
1268 | } |
1269 | } |
1270 | |
1271 | buf.pop(); |
1272 | }); |
1273 | |
1274 | xml.end_element(); |
1275 | } |
1276 | |
1277 | fn write_fill(fill: &Option<Fill>, is_clip_path: bool, opt: &WriteOptions, xml: &mut XmlWriter) { |
1278 | if let Some(ref fill: &Fill) = fill { |
1279 | write_paint(AId::Fill, &fill.paint, opt, xml); |
1280 | |
1281 | if fill.opacity != Opacity::ONE { |
1282 | xml.write_svg_attribute(id:AId::FillOpacity, &fill.opacity.get()); |
1283 | } |
1284 | |
1285 | if !fill.rule.is_default() { |
1286 | let name: AId = if is_clip_path { |
1287 | AId::ClipRule |
1288 | } else { |
1289 | AId::FillRule |
1290 | }; |
1291 | |
1292 | xml.write_svg_attribute(id:name, value:"evenodd" ); |
1293 | } |
1294 | } else { |
1295 | xml.write_svg_attribute(id:AId::Fill, value:"none" ); |
1296 | } |
1297 | } |
1298 | |
1299 | fn write_stroke(stroke: &Option<Stroke>, opt: &WriteOptions, xml: &mut XmlWriter) { |
1300 | if let Some(ref stroke) = stroke { |
1301 | write_paint(AId::Stroke, &stroke.paint, opt, xml); |
1302 | |
1303 | if stroke.opacity != Opacity::ONE { |
1304 | xml.write_svg_attribute(AId::StrokeOpacity, &stroke.opacity.get()); |
1305 | } |
1306 | |
1307 | if !stroke.dashoffset.approx_zero_ulps(4) { |
1308 | xml.write_svg_attribute(AId::StrokeDashoffset, &stroke.dashoffset) |
1309 | } |
1310 | |
1311 | if !stroke.miterlimit.is_default() { |
1312 | xml.write_svg_attribute(AId::StrokeMiterlimit, &stroke.miterlimit.get()); |
1313 | } |
1314 | |
1315 | if stroke.width.get() != 1.0 { |
1316 | xml.write_svg_attribute(AId::StrokeWidth, &stroke.width.get()); |
1317 | } |
1318 | |
1319 | match stroke.linecap { |
1320 | LineCap::Butt => {} |
1321 | LineCap::Round => xml.write_svg_attribute(AId::StrokeLinecap, "round" ), |
1322 | LineCap::Square => xml.write_svg_attribute(AId::StrokeLinecap, "square" ), |
1323 | } |
1324 | |
1325 | match stroke.linejoin { |
1326 | LineJoin::Miter => {} |
1327 | LineJoin::MiterClip => xml.write_svg_attribute(AId::StrokeLinejoin, "miter-clip" ), |
1328 | LineJoin::Round => xml.write_svg_attribute(AId::StrokeLinejoin, "round" ), |
1329 | LineJoin::Bevel => xml.write_svg_attribute(AId::StrokeLinejoin, "bevel" ), |
1330 | } |
1331 | |
1332 | if let Some(ref array) = stroke.dasharray { |
1333 | xml.write_numbers(AId::StrokeDasharray, array); |
1334 | } |
1335 | } else { |
1336 | // Always set `stroke` to `none` to override the parent value. |
1337 | // In 99.9% of the cases it's redundant, but a group with `filter` with `StrokePaint` |
1338 | // will set `stroke`, which will interfere with children nodes. |
1339 | xml.write_svg_attribute(AId::Stroke, "none" ); |
1340 | } |
1341 | } |
1342 | |
1343 | fn write_paint(aid: AId, paint: &Paint, opt: &WriteOptions, xml: &mut XmlWriter) { |
1344 | match paint { |
1345 | Paint::Color(c: &Color) => xml.write_color(id:aid, *c), |
1346 | Paint::LinearGradient(ref lg: &Arc) => { |
1347 | xml.write_func_iri(aid, lg.id(), opt); |
1348 | } |
1349 | Paint::RadialGradient(ref rg: &Arc) => { |
1350 | xml.write_func_iri(aid, rg.id(), opt); |
1351 | } |
1352 | Paint::Pattern(ref patt: &Arc) => { |
1353 | xml.write_func_iri(aid, patt.id(), opt); |
1354 | } |
1355 | } |
1356 | } |
1357 | |
1358 | fn write_light_source(light: &filter::LightSource, xml: &mut XmlWriter) { |
1359 | match light { |
1360 | filter::LightSource::DistantLight(ref light) => { |
1361 | xml.start_svg_element(EId::FeDistantLight); |
1362 | xml.write_svg_attribute(AId::Azimuth, &light.azimuth); |
1363 | xml.write_svg_attribute(AId::Elevation, &light.elevation); |
1364 | } |
1365 | filter::LightSource::PointLight(ref light) => { |
1366 | xml.start_svg_element(EId::FePointLight); |
1367 | xml.write_svg_attribute(AId::X, &light.x); |
1368 | xml.write_svg_attribute(AId::Y, &light.y); |
1369 | xml.write_svg_attribute(AId::Z, &light.z); |
1370 | } |
1371 | filter::LightSource::SpotLight(ref light) => { |
1372 | xml.start_svg_element(EId::FeSpotLight); |
1373 | xml.write_svg_attribute(AId::X, &light.x); |
1374 | xml.write_svg_attribute(AId::Y, &light.y); |
1375 | xml.write_svg_attribute(AId::Z, &light.z); |
1376 | xml.write_svg_attribute(AId::PointsAtX, &light.points_at_x); |
1377 | xml.write_svg_attribute(AId::PointsAtY, &light.points_at_y); |
1378 | xml.write_svg_attribute(AId::PointsAtZ, &light.points_at_z); |
1379 | xml.write_svg_attribute(AId::SpecularExponent, &light.specular_exponent); |
1380 | if let Some(ref n) = light.limiting_cone_angle { |
1381 | xml.write_svg_attribute(AId::LimitingConeAngle, n); |
1382 | } |
1383 | } |
1384 | } |
1385 | |
1386 | xml.end_element(); |
1387 | } |
1388 | |
1389 | static POW_VEC: &[f32] = &[ |
1390 | 1.0, |
1391 | 10.0, |
1392 | 100.0, |
1393 | 1_000.0, |
1394 | 10_000.0, |
1395 | 100_000.0, |
1396 | 1_000_000.0, |
1397 | 10_000_000.0, |
1398 | 100_000_000.0, |
1399 | 1_000_000_000.0, |
1400 | 10_000_000_000.0, |
1401 | 100_000_000_000.0, |
1402 | 1_000_000_000_000.0, |
1403 | ]; |
1404 | |
1405 | fn write_num(num: f32, buf: &mut Vec<u8>, precision: u8) { |
1406 | // If number is an integer, it's faster to write it as i32. |
1407 | if num.fract().approx_zero_ulps(4) { |
1408 | write!(buf, " {}" , num as i32).unwrap(); |
1409 | return; |
1410 | } |
1411 | |
1412 | // Round numbers up to the specified precision to prevent writing |
1413 | // ugly numbers like 29.999999999999996. |
1414 | // It's not 100% correct, but differences are insignificant. |
1415 | // |
1416 | // Note that at least in Rust 1.64 the number formatting in debug and release modes |
1417 | // can be slightly different. So having a lower precision makes |
1418 | // our output and tests reproducible. |
1419 | let v: f32 = (num * POW_VEC[precision as usize]).round() / POW_VEC[precision as usize]; |
1420 | |
1421 | write!(buf, " {}" , v).unwrap(); |
1422 | } |
1423 | |
1424 | /// Write all of the tspan attributes except for decorations. |
1425 | fn write_span( |
1426 | is_clip_path: bool, |
1427 | opt: &WriteOptions, |
1428 | xml: &mut XmlWriter, |
1429 | chunk: &TextChunk, |
1430 | span: &TextSpan, |
1431 | ) { |
1432 | xml.start_svg_element(EId::Tspan); |
1433 | |
1434 | let font_family_to_str = |font_family: &FontFamily| match font_family { |
1435 | FontFamily::Monospace => "monospace" .to_string(), |
1436 | FontFamily::Serif => "serif" .to_string(), |
1437 | FontFamily::SansSerif => "sans-serif" .to_string(), |
1438 | FontFamily::Cursive => "cursive" .to_string(), |
1439 | FontFamily::Fantasy => "fantasy" .to_string(), |
1440 | FontFamily::Named(s) => { |
1441 | // Only quote if absolutely necessary |
1442 | match parse_font_families(s) { |
1443 | Ok(_) => s.clone(), |
1444 | Err(_) => { |
1445 | if opt.use_single_quote { |
1446 | format!(" \"{}\"" , s) |
1447 | } else { |
1448 | format!("' {}'" , s) |
1449 | } |
1450 | } |
1451 | } |
1452 | } |
1453 | }; |
1454 | |
1455 | if !span.font.families.is_empty() { |
1456 | let families = span |
1457 | .font |
1458 | .families |
1459 | .iter() |
1460 | .map(font_family_to_str) |
1461 | .collect::<Vec<_>>() |
1462 | .join(", " ); |
1463 | xml.write_svg_attribute(AId::FontFamily, &families); |
1464 | } |
1465 | |
1466 | match span.font.style { |
1467 | FontStyle::Normal => {} |
1468 | FontStyle::Italic => xml.write_svg_attribute(AId::FontStyle, "italic" ), |
1469 | FontStyle::Oblique => xml.write_svg_attribute(AId::FontStyle, "oblique" ), |
1470 | } |
1471 | |
1472 | if span.font.weight != 400 { |
1473 | xml.write_svg_attribute(AId::FontWeight, &span.font.weight); |
1474 | } |
1475 | |
1476 | if span.font.stretch != FontStretch::Normal { |
1477 | let name = match span.font.stretch { |
1478 | FontStretch::Condensed => "condensed" , |
1479 | FontStretch::ExtraCondensed => "extra-condensed" , |
1480 | FontStretch::UltraCondensed => "ultra-condensed" , |
1481 | FontStretch::SemiCondensed => "semi-condensed" , |
1482 | FontStretch::Expanded => "expanded" , |
1483 | FontStretch::SemiExpanded => "semi-expanded" , |
1484 | FontStretch::ExtraExpanded => "extra-expanded" , |
1485 | FontStretch::UltraExpanded => "ultra-expanded" , |
1486 | FontStretch::Normal => unreachable!(), |
1487 | }; |
1488 | xml.write_svg_attribute(AId::FontStretch, name); |
1489 | } |
1490 | |
1491 | xml.write_svg_attribute(AId::FontSize, &span.font_size); |
1492 | |
1493 | xml.write_visibility(span.visible); |
1494 | |
1495 | if span.letter_spacing != 0.0 { |
1496 | xml.write_svg_attribute(AId::LetterSpacing, &span.letter_spacing); |
1497 | } |
1498 | |
1499 | if span.word_spacing != 0.0 { |
1500 | xml.write_svg_attribute(AId::WordSpacing, &span.word_spacing); |
1501 | } |
1502 | |
1503 | if let Some(text_length) = span.text_length { |
1504 | xml.write_svg_attribute(AId::TextLength, &text_length); |
1505 | } |
1506 | |
1507 | if span.length_adjust == LengthAdjust::SpacingAndGlyphs { |
1508 | xml.write_svg_attribute(AId::LengthAdjust, "spacingAndGlyphs" ); |
1509 | } |
1510 | |
1511 | if span.small_caps { |
1512 | xml.write_svg_attribute(AId::FontVariant, "small-caps" ); |
1513 | } |
1514 | |
1515 | if span.paint_order == PaintOrder::StrokeAndFill { |
1516 | xml.write_svg_attribute(AId::PaintOrder, "stroke fill" ); |
1517 | } |
1518 | |
1519 | if !span.apply_kerning { |
1520 | xml.write_attribute("style" , "font-kerning:none" ) |
1521 | } |
1522 | |
1523 | if span.dominant_baseline != DominantBaseline::Auto { |
1524 | let name = match span.dominant_baseline { |
1525 | DominantBaseline::UseScript => "use-script" , |
1526 | DominantBaseline::NoChange => "no-change" , |
1527 | DominantBaseline::ResetSize => "reset-size" , |
1528 | DominantBaseline::TextBeforeEdge => "text-before-edge" , |
1529 | DominantBaseline::Middle => "middle" , |
1530 | DominantBaseline::Central => "central" , |
1531 | DominantBaseline::TextAfterEdge => "text-after-edge" , |
1532 | DominantBaseline::Ideographic => "ideographic" , |
1533 | DominantBaseline::Alphabetic => "alphabetic" , |
1534 | DominantBaseline::Hanging => "hanging" , |
1535 | DominantBaseline::Mathematical => "mathematical" , |
1536 | DominantBaseline::Auto => unreachable!(), |
1537 | }; |
1538 | xml.write_svg_attribute(AId::DominantBaseline, name); |
1539 | } |
1540 | |
1541 | if span.alignment_baseline != AlignmentBaseline::Auto { |
1542 | let name = match span.alignment_baseline { |
1543 | AlignmentBaseline::Baseline => "baseline" , |
1544 | AlignmentBaseline::BeforeEdge => "before-edge" , |
1545 | AlignmentBaseline::TextBeforeEdge => "text-before-edge" , |
1546 | AlignmentBaseline::Middle => "middle" , |
1547 | AlignmentBaseline::Central => "central" , |
1548 | AlignmentBaseline::AfterEdge => "after-edge" , |
1549 | AlignmentBaseline::TextAfterEdge => "text-after-edge" , |
1550 | AlignmentBaseline::Ideographic => "ideographic" , |
1551 | AlignmentBaseline::Alphabetic => "alphabetic" , |
1552 | AlignmentBaseline::Hanging => "hanging" , |
1553 | AlignmentBaseline::Mathematical => "mathematical" , |
1554 | AlignmentBaseline::Auto => unreachable!(), |
1555 | }; |
1556 | xml.write_svg_attribute(AId::AlignmentBaseline, name); |
1557 | } |
1558 | |
1559 | write_fill(&span.fill, is_clip_path, opt, xml); |
1560 | write_stroke(&span.stroke, opt, xml); |
1561 | |
1562 | for baseline_shift in &span.baseline_shift { |
1563 | xml.start_svg_element(EId::Tspan); |
1564 | match baseline_shift { |
1565 | BaselineShift::Baseline => {} |
1566 | BaselineShift::Number(num) => xml.write_svg_attribute(AId::BaselineShift, num), |
1567 | BaselineShift::Subscript => xml.write_svg_attribute(AId::BaselineShift, "sub" ), |
1568 | BaselineShift::Superscript => xml.write_svg_attribute(AId::BaselineShift, "super" ), |
1569 | } |
1570 | } |
1571 | |
1572 | let cur_text = &chunk.text[span.start..span.end]; |
1573 | |
1574 | xml.write_text(&cur_text.replace('&' , "&" )); |
1575 | |
1576 | // End for each tspan we needed to create for baseline_shift |
1577 | for _ in &span.baseline_shift { |
1578 | xml.end_element(); |
1579 | } |
1580 | |
1581 | xml.end_element(); |
1582 | } |
1583 | |