/// <summary> /// Sets the path on this element and all child elements. Uses the state /// object to track the state of the drawing /// </summary> /// <param name="state">State of the drawing operation</param> /// <param name="doMeasurements">If true, calculate and apply text length adjustments.</param> private void SetPath(TextDrawingState state, bool doMeasurements) { TextDrawingState origState = null; bool alignOnBaseline = state.BaselinePath != null && (this.TextAnchor == SvgTextAnchor.Middle || this.TextAnchor == SvgTextAnchor.End); if (doMeasurements) { if (this.TextLength != SvgUnit.None) { origState = state.Clone(); } else if (alignOnBaseline) { origState = state.Clone(); state.BaselinePath = null; } } foreach (var node in GetContentNodes()) { SvgTextBase textNode = node as SvgTextBase; if (textNode == null) { if (!string.IsNullOrEmpty(node.Content)) { state.DrawString(PrepareText(node.Content)); } } else { TextDrawingState newState = new TextDrawingState(state, textNode); textNode.SetPath(newState); state.NumChars += newState.NumChars; state.Current = newState.Current; } } var path = state.GetPath() ?? new GraphicsPath(); // Apply any text length adjustments if (doMeasurements) { if (this.TextLength != SvgUnit.None) { var specLength = this.TextLength.ToDeviceValue(state.Renderer, UnitRenderingType.Horizontal, this); var actLength = state.TextBounds.Width; var diff = (actLength - specLength); if (Math.Abs(diff) > 1.5) { if (this.LengthAdjust == SvgTextLengthAdjust.Spacing) { if (this.X.Count < 2) { var numCharDiff = state.NumChars - origState.NumChars - 1; if (numCharDiff != 0) { origState.LetterSpacingAdjust = -1 * diff / numCharDiff; SetPath(origState, false); return; } } } else { using (var matrix = new Matrix()) { matrix.Translate(-1 * state.TextBounds.X, 0, MatrixOrder.Append); matrix.Scale(specLength / actLength, 1, MatrixOrder.Append); matrix.Translate(state.TextBounds.X, 0, MatrixOrder.Append); path.Transform(matrix); } } } } else if (alignOnBaseline) { var bounds = path.GetBounds(); if (this.TextAnchor == SvgTextAnchor.Middle) { origState.StartOffsetAdjust = -1 * bounds.Width / 2; } else { origState.StartOffsetAdjust = -1 * bounds.Width; } SetPath(origState, false); return; } } _path = path; this.IsPathDirty = false; }
/// <summary> /// Sets the path on this element and all child elements. Uses the state /// object to track the state of the drawing /// </summary> /// <param name="state">State of the drawing operation</param> private void SetPath(TextDrawingState state, bool doMeasurements) { TextDrawingState origState = null; bool alignOnBaseline = state.BaselinePath != null && (this.TextAnchor == SvgTextAnchor.Middle || this.TextAnchor == SvgTextAnchor.End); if (doMeasurements) { if (this.TextLength != SvgUnit.None) { origState = state.Clone(); } else if (alignOnBaseline) { origState = state.Clone(); state.BaselinePath = null; } } foreach (var node in GetContentNodes()) { SvgTextBase textNode = node as SvgTextBase; if (textNode == null) { if (!string.IsNullOrEmpty(node.Content)) { state.DrawString(PrepareText(node.Content)); } } else { TextDrawingState newState = new TextDrawingState(state, textNode); textNode.SetPath(newState); state.NumChars += newState.NumChars; state.Current = newState.Current; } } var path = state.GetPath() ?? new GraphicsPath(); // Apply any text length adjustments if (doMeasurements) { if (this.TextLength != SvgUnit.None) { var bounds = path.GetBounds(); var specLength = this.TextLength.ToDeviceValue(state.Renderer, UnitRenderingType.Horizontal, this); var actLength = bounds.Width; var diff = (actLength - specLength); if (Math.Abs(diff) > 1.5) { if (this.LengthAdjust == SvgTextLengthAdjust.spacing) { origState.LetterSpacingAdjust = -1 * diff / (state.NumChars - origState.NumChars - 1); SetPath(origState, false); return; } else { using (var matrix = new Matrix()) { matrix.Translate(-1 * bounds.X, 0, MatrixOrder.Append); matrix.Scale(specLength / actLength, 1, MatrixOrder.Append); matrix.Translate(bounds.X, 0, MatrixOrder.Append); path.Transform(matrix); } } } } else if (alignOnBaseline) { var bounds = path.GetBounds(); if (this.TextAnchor == SvgTextAnchor.Middle) { origState.StartOffsetAdjust = -1 * bounds.Width / 2; } else { origState.StartOffsetAdjust = -1 * bounds.Width; } SetPath(origState, false); return; } } _path = path; this.IsPathDirty = false; // If we have child tspans we need to correct the position to account for text-anchor var childTSpans = GetContentNodes().OfType <SvgTextSpan>().ToList(); if (childTSpans.Count > 1) // If there's only one tspan we'll allow it to adjust itself in FlushPath() { if (!(TextAnchor == SvgTextAnchor.Middle || TextAnchor == SvgTextAnchor.End)) { return; } // BUG: If we have rotations things break // Skip them for now if (this.Transforms.OfType <SvgRotate>().Any()) { return; } // Need to split tspans by line // An element with an x attribute set seems to start a new render line var lines = new List <List <SvgTextSpan> >(); foreach (var tspan in childTSpans) { if (lines.Count == 0 || tspan.X.Any()) { lines.Add(new List <SvgTextSpan>()); } lines.Last().Add(tspan); } foreach (var line in lines) { // Find total width of line var totalLineWidth = line.Sum(o => o.Bounds.Width); // BUG: If we have tspans with spaces in the source between them (like '<a></a> <b></b>'), they should be // rendered with that space, but there's a bug elsewhere which does this wrong and ignores it. // I'm also ignoring it here, because I don't know how to fix this correctly. float xOffset = 0; switch (TextAnchor) { case SvgTextAnchor.Middle: xOffset = -totalLineWidth / 2; break; case SvgTextAnchor.End: xOffset = -totalLineWidth; break; default: throw new InvalidOperationException("Shouldn't have got here with TextAnchor " + TextAnchor); } using (var matrix = new Matrix()) { matrix.Translate(xOffset, 0); foreach (var node in line) { node._path.Transform(matrix); } } } // The path for the parent text node renders a black copy of the text for some reason _path.Reset(); } }