[12762] | 1 | using System;
|
---|
| 2 | using System.Xml;
|
---|
| 3 | using System.Drawing;
|
---|
| 4 | using System.Drawing.Drawing2D;
|
---|
| 5 | using System.Text;
|
---|
| 6 | using System.Text.RegularExpressions;
|
---|
| 7 |
|
---|
| 8 | using SharpVectors.Dom.Css;
|
---|
| 9 | using SharpVectors.Dom.Svg;
|
---|
| 10 |
|
---|
| 11 | namespace SharpVectors.Renderers.Gdi
|
---|
| 12 | {
|
---|
| 13 | public sealed class GdiTextRendering : GdiRendering
|
---|
| 14 | {
|
---|
| 15 | #region Private Fields
|
---|
| 16 |
|
---|
| 17 | private GdiGraphicsWrapper _graphics;
|
---|
| 18 |
|
---|
| 19 | #endregion
|
---|
| 20 |
|
---|
| 21 | #region Constructor and Destructor
|
---|
| 22 |
|
---|
| 23 | public GdiTextRendering(SvgElement element)
|
---|
| 24 | : base(element)
|
---|
| 25 | {
|
---|
| 26 | }
|
---|
| 27 |
|
---|
| 28 | #endregion
|
---|
| 29 |
|
---|
| 30 | #region Public Properties
|
---|
| 31 |
|
---|
| 32 | public override bool IsRecursive
|
---|
| 33 | {
|
---|
| 34 | get
|
---|
| 35 | {
|
---|
| 36 | return true;
|
---|
| 37 | }
|
---|
| 38 | }
|
---|
| 39 |
|
---|
| 40 | #endregion
|
---|
| 41 |
|
---|
| 42 | #region Public Methods
|
---|
| 43 |
|
---|
| 44 | public override void BeforeRender(GdiGraphicsRenderer renderer)
|
---|
| 45 | {
|
---|
| 46 | if (_uniqueColor.IsEmpty)
|
---|
| 47 | _uniqueColor = renderer.GetNextColor(element);
|
---|
| 48 |
|
---|
| 49 | GdiGraphicsWrapper graphics = renderer.GraphicsWrapper;
|
---|
| 50 |
|
---|
| 51 | _graphicsContainer = graphics.BeginContainer();
|
---|
| 52 | SetQuality(graphics);
|
---|
| 53 | Transform(graphics);
|
---|
| 54 | }
|
---|
| 55 |
|
---|
| 56 | public override void Render(GdiGraphicsRenderer renderer)
|
---|
| 57 | {
|
---|
| 58 | _graphics = renderer.GraphicsWrapper;
|
---|
| 59 |
|
---|
| 60 | SvgRenderingHint hint = element.RenderingHint;
|
---|
| 61 | if (hint == SvgRenderingHint.Clipping)
|
---|
| 62 | {
|
---|
| 63 | return;
|
---|
| 64 | }
|
---|
| 65 | if (element.ParentNode is SvgClipPathElement)
|
---|
| 66 | {
|
---|
| 67 | return;
|
---|
| 68 | }
|
---|
| 69 |
|
---|
| 70 | SvgTextElement textElement = element as SvgTextElement;
|
---|
| 71 | if (textElement == null)
|
---|
| 72 | {
|
---|
| 73 | return;
|
---|
| 74 | }
|
---|
| 75 |
|
---|
| 76 | string sVisibility = textElement.GetPropertyValue("visibility");
|
---|
| 77 | string sDisplay = textElement.GetPropertyValue("display");
|
---|
| 78 | if (String.Equals(sVisibility, "hidden") || String.Equals(sDisplay, "none"))
|
---|
| 79 | {
|
---|
| 80 | return;
|
---|
| 81 | }
|
---|
| 82 |
|
---|
| 83 | Clip(_graphics);
|
---|
| 84 |
|
---|
| 85 | PointF ctp = new PointF(0, 0); // current text position
|
---|
| 86 |
|
---|
| 87 | ctp = GetCurrentTextPosition(textElement, ctp);
|
---|
| 88 | string sBaselineShift = textElement.GetPropertyValue("baseline-shift").Trim();
|
---|
| 89 | double shiftBy = 0;
|
---|
| 90 |
|
---|
| 91 | if (sBaselineShift.Length > 0)
|
---|
| 92 | {
|
---|
| 93 | float textFontSize = GetComputedFontSize(textElement);
|
---|
| 94 | if (sBaselineShift.EndsWith("%"))
|
---|
| 95 | {
|
---|
| 96 | shiftBy = SvgNumber.ParseNumber(sBaselineShift.Substring(0,
|
---|
| 97 | sBaselineShift.Length - 1)) / 100 * textFontSize;
|
---|
| 98 | }
|
---|
| 99 | else if (sBaselineShift == "sub")
|
---|
| 100 | {
|
---|
| 101 | shiftBy = -0.6F * textFontSize;
|
---|
| 102 | }
|
---|
| 103 | else if (sBaselineShift == "super")
|
---|
| 104 | {
|
---|
| 105 | shiftBy = 0.6F * textFontSize;
|
---|
| 106 | }
|
---|
| 107 | else if (sBaselineShift == "baseline")
|
---|
| 108 | {
|
---|
| 109 | shiftBy = 0;
|
---|
| 110 | }
|
---|
| 111 | else
|
---|
| 112 | {
|
---|
| 113 | shiftBy = SvgNumber.ParseNumber(sBaselineShift);
|
---|
| 114 | }
|
---|
| 115 | }
|
---|
| 116 |
|
---|
| 117 | XmlNodeType nodeType = XmlNodeType.None;
|
---|
| 118 | foreach (XmlNode child in element.ChildNodes)
|
---|
| 119 | {
|
---|
| 120 | nodeType = child.NodeType;
|
---|
| 121 | if (nodeType == XmlNodeType.Text)
|
---|
| 122 | {
|
---|
| 123 | ctp.Y -= (float)shiftBy;
|
---|
| 124 | AddGraphicsPath(textElement, ref ctp, GetText(textElement, child));
|
---|
| 125 | ctp.Y += (float)shiftBy;
|
---|
| 126 | }
|
---|
| 127 | else if (nodeType == XmlNodeType.Element)
|
---|
| 128 | {
|
---|
| 129 | string nodeName = child.Name;
|
---|
| 130 | if (String.Equals(nodeName, "tref"))
|
---|
| 131 | {
|
---|
| 132 | AddTRefElementPath((SvgTRefElement)child, ref ctp);
|
---|
| 133 | }
|
---|
| 134 | else if (String.Equals(nodeName, "tspan"))
|
---|
| 135 | {
|
---|
| 136 | AddTSpanElementPath((SvgTSpanElement)child, ref ctp);
|
---|
| 137 | }
|
---|
| 138 | }
|
---|
| 139 | }
|
---|
| 140 |
|
---|
| 141 | PaintMarkers(renderer, textElement, _graphics);
|
---|
| 142 |
|
---|
| 143 | _graphics = null;
|
---|
| 144 | }
|
---|
| 145 |
|
---|
| 146 | #endregion
|
---|
| 147 |
|
---|
| 148 | #region Private Methods
|
---|
| 149 |
|
---|
| 150 | private Brush GetBrush(GraphicsPath gp)
|
---|
| 151 | {
|
---|
| 152 | GdiSvgPaint paint = new GdiSvgPaint(element as SvgStyleableElement, "fill");
|
---|
| 153 | return paint.GetBrush(gp);
|
---|
| 154 | }
|
---|
| 155 |
|
---|
| 156 | private Pen GetPen(GraphicsPath gp)
|
---|
| 157 | {
|
---|
| 158 | GdiSvgPaint paint = new GdiSvgPaint(element as SvgStyleableElement, "stroke");
|
---|
| 159 | return paint.GetPen(gp);
|
---|
| 160 | }
|
---|
| 161 |
|
---|
| 162 | #region Private Text Methods
|
---|
| 163 |
|
---|
| 164 | private string TrimText(SvgTextContentElement element, string val)
|
---|
| 165 | {
|
---|
| 166 | Regex tabNewline = new Regex(@"[\n\f\t]");
|
---|
| 167 | if (element.XmlSpace != "preserve")
|
---|
| 168 | val = val.Replace("\n", String.Empty);
|
---|
| 169 | val = tabNewline.Replace(val, " ");
|
---|
| 170 |
|
---|
| 171 | if (element.XmlSpace == "preserve")
|
---|
| 172 | return val;
|
---|
| 173 | else
|
---|
| 174 | return val.Trim();
|
---|
| 175 | }
|
---|
| 176 |
|
---|
| 177 | private string GetText(SvgTextContentElement element, XmlNode child)
|
---|
| 178 | {
|
---|
| 179 | return TrimText(element, child.Value);
|
---|
| 180 | }
|
---|
| 181 |
|
---|
| 182 | private void AddGraphicsPath(SvgTextContentElement element, ref PointF ctp, string text)
|
---|
| 183 | {
|
---|
| 184 | if (text.Length == 0)
|
---|
| 185 | return;
|
---|
| 186 |
|
---|
| 187 | float emSize = GetComputedFontSize(element);
|
---|
| 188 | FontFamily family = GetGDIFontFamily(element, emSize);
|
---|
| 189 | int style = GetGDIFontStyle(element);
|
---|
| 190 | StringFormat sf = GetGDIStringFormat(element);
|
---|
| 191 |
|
---|
| 192 | GraphicsPath textGeometry = new GraphicsPath();
|
---|
| 193 |
|
---|
| 194 | float xCorrection = 0;
|
---|
| 195 | if (sf.Alignment == StringAlignment.Near)
|
---|
| 196 | xCorrection = emSize * 1 / 6;
|
---|
| 197 | else if (sf.Alignment == StringAlignment.Far)
|
---|
| 198 | xCorrection = -emSize * 1 / 6;
|
---|
| 199 |
|
---|
| 200 | float yCorrection = (float)(family.GetCellAscent(FontStyle.Regular)) / (float)(family.GetEmHeight(FontStyle.Regular)) * emSize;
|
---|
| 201 |
|
---|
| 202 | // TODO: font property
|
---|
| 203 | PointF p = new PointF(ctp.X - xCorrection, ctp.Y - yCorrection);
|
---|
| 204 |
|
---|
| 205 | textGeometry.AddString(text, family, style, emSize, p, sf);
|
---|
| 206 | if (!textGeometry.GetBounds().IsEmpty)
|
---|
| 207 | {
|
---|
| 208 | float bboxWidth = textGeometry.GetBounds().Width;
|
---|
| 209 | if (sf.Alignment == StringAlignment.Center)
|
---|
| 210 | bboxWidth /= 2;
|
---|
| 211 | else if (sf.Alignment == StringAlignment.Far)
|
---|
| 212 | bboxWidth = 0;
|
---|
| 213 |
|
---|
| 214 | ctp.X += bboxWidth + emSize / 4;
|
---|
| 215 | }
|
---|
| 216 |
|
---|
| 217 | GdiSvgPaint fillPaint = new GdiSvgPaint(element, "fill");
|
---|
| 218 | Brush brush = fillPaint.GetBrush(textGeometry);
|
---|
| 219 |
|
---|
| 220 | GdiSvgPaint strokePaint = new GdiSvgPaint(element, "stroke");
|
---|
| 221 | Pen pen = strokePaint.GetPen(textGeometry);
|
---|
| 222 |
|
---|
| 223 | if (brush != null)
|
---|
| 224 | {
|
---|
| 225 | if (brush is PathGradientBrush)
|
---|
| 226 | {
|
---|
| 227 | GdiGradientFill gps = fillPaint.PaintFill as GdiGradientFill;
|
---|
| 228 |
|
---|
| 229 | _graphics.SetClip(gps.GetRadialGradientRegion(textGeometry.GetBounds()), CombineMode.Exclude);
|
---|
| 230 |
|
---|
| 231 | SolidBrush tempBrush = new SolidBrush(((PathGradientBrush)brush).InterpolationColors.Colors[0]);
|
---|
| 232 | _graphics.FillPath(this, tempBrush, textGeometry);
|
---|
| 233 | tempBrush.Dispose();
|
---|
| 234 | _graphics.ResetClip();
|
---|
| 235 | }
|
---|
| 236 |
|
---|
| 237 | _graphics.FillPath(this, brush, textGeometry);
|
---|
| 238 | brush.Dispose();
|
---|
| 239 | }
|
---|
| 240 |
|
---|
| 241 | if (pen != null)
|
---|
| 242 | {
|
---|
| 243 | if (pen.Brush is PathGradientBrush)
|
---|
| 244 | {
|
---|
| 245 | GdiGradientFill gps = strokePaint.PaintFill as GdiGradientFill;
|
---|
| 246 | GdiGraphicsContainer container = _graphics.BeginContainer();
|
---|
| 247 |
|
---|
| 248 | _graphics.SetClip(gps.GetRadialGradientRegion(textGeometry.GetBounds()), CombineMode.Exclude);
|
---|
| 249 |
|
---|
| 250 | SolidBrush tempBrush = new SolidBrush(((PathGradientBrush)pen.Brush).InterpolationColors.Colors[0]);
|
---|
| 251 | Pen tempPen = new Pen(tempBrush, pen.Width);
|
---|
| 252 | _graphics.DrawPath(this, tempPen, textGeometry);
|
---|
| 253 | tempPen.Dispose();
|
---|
| 254 | tempBrush.Dispose();
|
---|
| 255 |
|
---|
| 256 | _graphics.EndContainer(container);
|
---|
| 257 | }
|
---|
| 258 |
|
---|
| 259 | _graphics.DrawPath(this, pen, textGeometry);
|
---|
| 260 | pen.Dispose();
|
---|
| 261 | }
|
---|
| 262 |
|
---|
| 263 | textGeometry.Dispose();
|
---|
| 264 | }
|
---|
| 265 |
|
---|
| 266 | public string GetTRefText(SvgTRefElement element)
|
---|
| 267 | {
|
---|
| 268 | XmlElement refElement = element.ReferencedElement;
|
---|
| 269 | if (refElement != null)
|
---|
| 270 | {
|
---|
| 271 | return TrimText(element, refElement.InnerText);
|
---|
| 272 | }
|
---|
| 273 | else
|
---|
| 274 | {
|
---|
| 275 | return String.Empty;
|
---|
| 276 | }
|
---|
| 277 | }
|
---|
| 278 |
|
---|
| 279 | private void AddTRefElementPath(SvgTRefElement element, ref PointF ctp)
|
---|
| 280 | {
|
---|
| 281 | ctp = GetCurrentTextPosition(element, ctp);
|
---|
| 282 |
|
---|
| 283 | this.AddGraphicsPath(element, ref ctp, GetTRefText(element));
|
---|
| 284 | }
|
---|
| 285 |
|
---|
| 286 | private void AddTSpanElementPath(SvgTSpanElement element, ref PointF ctp)
|
---|
| 287 | {
|
---|
| 288 | ctp = GetCurrentTextPosition(element, ctp);
|
---|
| 289 | string sBaselineShift = element.GetPropertyValue("baseline-shift").Trim();
|
---|
| 290 | double shiftBy = 0;
|
---|
| 291 |
|
---|
| 292 | if (sBaselineShift.Length > 0)
|
---|
| 293 | {
|
---|
| 294 | SvgTextElement textElement = (SvgTextElement)element.SelectSingleNode("ancestor::svg:text",
|
---|
| 295 | element.OwnerDocument.NamespaceManager);
|
---|
| 296 |
|
---|
| 297 | float textFontSize = GetComputedFontSize(textElement);
|
---|
| 298 | if (sBaselineShift.EndsWith("%"))
|
---|
| 299 | {
|
---|
| 300 | shiftBy = SvgNumber.ParseNumber(sBaselineShift.Substring(0,
|
---|
| 301 | sBaselineShift.Length - 1)) / 100 * textFontSize;
|
---|
| 302 | }
|
---|
| 303 | else if (sBaselineShift == "sub")
|
---|
| 304 | {
|
---|
| 305 | shiftBy = -0.6F * textFontSize;
|
---|
| 306 | }
|
---|
| 307 | else if (sBaselineShift == "super")
|
---|
| 308 | {
|
---|
| 309 | shiftBy = 0.6F * textFontSize;
|
---|
| 310 | }
|
---|
| 311 | else if (sBaselineShift == "baseline")
|
---|
| 312 | {
|
---|
| 313 | shiftBy = 0;
|
---|
| 314 | }
|
---|
| 315 | else
|
---|
| 316 | {
|
---|
| 317 | shiftBy = SvgNumber.ParseNumber(sBaselineShift);
|
---|
| 318 | }
|
---|
| 319 | }
|
---|
| 320 |
|
---|
| 321 | foreach (XmlNode child in element.ChildNodes)
|
---|
| 322 | {
|
---|
| 323 | if (child.NodeType == XmlNodeType.Text)
|
---|
| 324 | {
|
---|
| 325 | ctp.Y -= (float)shiftBy;
|
---|
| 326 | AddGraphicsPath(element, ref ctp, GetText(element, child));
|
---|
| 327 | ctp.Y += (float)shiftBy;
|
---|
| 328 | }
|
---|
| 329 | }
|
---|
| 330 | }
|
---|
| 331 |
|
---|
| 332 | private PointF GetCurrentTextPosition(SvgTextPositioningElement posElement, PointF p)
|
---|
| 333 | {
|
---|
| 334 | if (posElement.X.AnimVal.NumberOfItems > 0)
|
---|
| 335 | {
|
---|
| 336 | p.X = (float)posElement.X.AnimVal.GetItem(0).Value;
|
---|
| 337 | }
|
---|
| 338 | if (posElement.Y.AnimVal.NumberOfItems > 0)
|
---|
| 339 | {
|
---|
| 340 | p.Y = (float)posElement.Y.AnimVal.GetItem(0).Value;
|
---|
| 341 | }
|
---|
| 342 | if (posElement.Dx.AnimVal.NumberOfItems > 0)
|
---|
| 343 | {
|
---|
| 344 | p.X += (float)posElement.Dx.AnimVal.GetItem(0).Value;
|
---|
| 345 | }
|
---|
| 346 | if (posElement.Dy.AnimVal.NumberOfItems > 0)
|
---|
| 347 | {
|
---|
| 348 | p.Y += (float)posElement.Dy.AnimVal.GetItem(0).Value;
|
---|
| 349 | }
|
---|
| 350 | return p;
|
---|
| 351 | }
|
---|
| 352 |
|
---|
| 353 | private int GetGDIFontStyle(SvgTextContentElement element)
|
---|
| 354 | {
|
---|
| 355 | int style = (int)FontStyle.Regular;
|
---|
| 356 | string fontWeight = element.GetPropertyValue("font-weight");
|
---|
| 357 | if (fontWeight == "bold" || fontWeight == "bolder" || fontWeight == "600" || fontWeight == "700" || fontWeight == "800" || fontWeight == "900")
|
---|
| 358 | {
|
---|
| 359 | style = style | (int)FontStyle.Bold;
|
---|
| 360 | }
|
---|
| 361 |
|
---|
| 362 | if (element.GetPropertyValue("font-style") == "italic")
|
---|
| 363 | {
|
---|
| 364 | style = style | (int)FontStyle.Italic;
|
---|
| 365 | }
|
---|
| 366 |
|
---|
| 367 | string textDeco = element.GetPropertyValue("text-decoration");
|
---|
| 368 | if (textDeco == "line-through")
|
---|
| 369 | {
|
---|
| 370 | style = style | (int)FontStyle.Strikeout;
|
---|
| 371 | }
|
---|
| 372 | else if (textDeco == "underline")
|
---|
| 373 | {
|
---|
| 374 | style = style | (int)FontStyle.Underline;
|
---|
| 375 | }
|
---|
| 376 | return style;
|
---|
| 377 | }
|
---|
| 378 |
|
---|
| 379 | private FontFamily GetGDIFontFamily(SvgTextContentElement element, float fontSize)
|
---|
| 380 | {
|
---|
| 381 | string fontFamily = element.GetPropertyValue("font-family");
|
---|
| 382 | string[] fontNames = fontNames = fontFamily.Split(new char[1] { ',' });
|
---|
| 383 |
|
---|
| 384 | FontFamily family;
|
---|
| 385 |
|
---|
| 386 | foreach (string fn in fontNames)
|
---|
| 387 | {
|
---|
| 388 | try
|
---|
| 389 | {
|
---|
| 390 | string fontName = fn.Trim(new char[] { ' ', '\'', '"' });
|
---|
| 391 |
|
---|
| 392 | if (fontName == "serif")
|
---|
| 393 | family = FontFamily.GenericSerif;
|
---|
| 394 | else if (fontName == "sans-serif")
|
---|
| 395 | family = FontFamily.GenericSansSerif;
|
---|
| 396 | else if (fontName == "monospace")
|
---|
| 397 | family = FontFamily.GenericMonospace;
|
---|
| 398 | else
|
---|
| 399 | family = new FontFamily(fontName); // Font(,fontSize).FontFamily;
|
---|
| 400 |
|
---|
| 401 | return family;
|
---|
| 402 | }
|
---|
| 403 | catch
|
---|
| 404 | {
|
---|
| 405 | }
|
---|
| 406 | }
|
---|
| 407 |
|
---|
| 408 | // no known font-family was found => default to arial
|
---|
| 409 | return new FontFamily("Arial");
|
---|
| 410 | }
|
---|
| 411 |
|
---|
| 412 | private StringFormat GetGDIStringFormat(SvgTextContentElement element)
|
---|
| 413 | {
|
---|
| 414 | StringFormat sf = new StringFormat();
|
---|
| 415 |
|
---|
| 416 | bool doAlign = true;
|
---|
| 417 | if (element is SvgTSpanElement || element is SvgTRefElement)
|
---|
| 418 | {
|
---|
| 419 | SvgTextPositioningElement posElement = (SvgTextPositioningElement)element;
|
---|
| 420 | if (posElement.X.AnimVal.NumberOfItems == 0) doAlign = false;
|
---|
| 421 | }
|
---|
| 422 |
|
---|
| 423 | if (doAlign)
|
---|
| 424 | {
|
---|
| 425 | string anchor = element.GetPropertyValue("text-anchor");
|
---|
| 426 | if (anchor == "middle")
|
---|
| 427 | sf.Alignment = StringAlignment.Center;
|
---|
| 428 | if (anchor == "end")
|
---|
| 429 | sf.Alignment = StringAlignment.Far;
|
---|
| 430 | }
|
---|
| 431 |
|
---|
| 432 | string dir = element.GetPropertyValue("direction");
|
---|
| 433 | if (dir == "rtl")
|
---|
| 434 | {
|
---|
| 435 | if (sf.Alignment == StringAlignment.Far)
|
---|
| 436 | sf.Alignment = StringAlignment.Near;
|
---|
| 437 | else if (sf.Alignment == StringAlignment.Near)
|
---|
| 438 | sf.Alignment = StringAlignment.Far;
|
---|
| 439 | sf.FormatFlags = StringFormatFlags.DirectionRightToLeft;
|
---|
| 440 | }
|
---|
| 441 |
|
---|
| 442 | dir = element.GetPropertyValue("writing-mode");
|
---|
| 443 | if (dir == "tb")
|
---|
| 444 | {
|
---|
| 445 | sf.FormatFlags = sf.FormatFlags | StringFormatFlags.DirectionVertical;
|
---|
| 446 | }
|
---|
| 447 |
|
---|
| 448 | sf.FormatFlags = sf.FormatFlags | StringFormatFlags.MeasureTrailingSpaces;
|
---|
| 449 |
|
---|
| 450 | return sf;
|
---|
| 451 | }
|
---|
| 452 |
|
---|
| 453 | private float GetComputedFontSize(SvgTextContentElement element)
|
---|
| 454 | {
|
---|
| 455 | string str = element.GetPropertyValue("font-size");
|
---|
| 456 | float fontSize = 12;
|
---|
| 457 | if (str.EndsWith("%"))
|
---|
| 458 | {
|
---|
| 459 | // percentage of inherited value
|
---|
| 460 | }
|
---|
| 461 | else if (new Regex(@"^\d").IsMatch(str))
|
---|
| 462 | {
|
---|
| 463 | // svg length
|
---|
| 464 | fontSize = (float)new SvgLength(element, "font-size",
|
---|
| 465 | SvgLengthDirection.Viewport, str, "10px").Value;
|
---|
| 466 | }
|
---|
| 467 | else if (str == "larger")
|
---|
| 468 | {
|
---|
| 469 | }
|
---|
| 470 | else if (str == "smaller")
|
---|
| 471 | {
|
---|
| 472 |
|
---|
| 473 | }
|
---|
| 474 | else
|
---|
| 475 | {
|
---|
| 476 | // check for absolute value
|
---|
| 477 | }
|
---|
| 478 |
|
---|
| 479 | return fontSize;
|
---|
| 480 | }
|
---|
| 481 |
|
---|
| 482 | #endregion
|
---|
| 483 |
|
---|
| 484 | #endregion
|
---|
| 485 | }
|
---|
| 486 | }
|
---|