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