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