| 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 | |