0
votes

I'm trying to draw an SVG path in a PDF using itextsharp v5.

The approach I am following is roughly this:

  1. Reading the SVG path from the SVG file (Svg.SvgPath)
  2. Getting the list of segments from the path ({Svg.Pathing.SvgPathSegmentList})
  3. Creating an iTextSharp PdfAnnotation and associate a PdfAppearance to it
  4. Drawing each segment in the SvgPathSegmentList using the corresponding PdfContentByte method ( for SvgLineSegment I use PdfContentByte.LineTo, for SvgCubicCurveSegment I use PdfContentByte.CurveTo )

For most of the SvgPathSegments types, there is a clear mapping between values in the SvgPathSegments and the arguments in the PdfContentByte method. A few examples:

  • SvgMoveToSegment has the attribute End which is the target point (X, Y) and the PdfContentByte.MoveTo takes two parameters: X, Y

  • SvgLineSegment, very similar to the Move. It has the Target End and the PdfContentByte.LineTo takes two parameters X and Y and draws a line from the current position to the target point.

     app.MoveTo(segment.Start.X, segment.Start.Y);
    
  • SvgCubicCurveSegment has all you need to create a Bezier curve (The Start point, the End point, and the first and second control point). With this I use PdfContentByte.CurveTo and get a curve in the PDF that looks exactly as it looks in the SVG editor.

     var cubicCurve = (Svg.Pathing.SvgCubicCurveSegment)segment;
     app.CurveTo(                             
             cubicCurve.FirstControlPoint.X, cubicCurve.FirstControlPoint.Y, 
             cubicCurve.SecondControlPoint.X, cubicCurve.SecondControlPoint.Y,
             cubicCurve.End.X, cubicCurve.End.Y);
    

The problem I have is with the ARC ("A" command in the SVG, SvgArcSegment)

The SvgArcSegment has the following values:

  • Angle
  • Start (X, Y)
  • End (X, Y)
  • RadiusX
  • RadiusY
  • Start
  • Sweep

On the other hand, PdfContentByte.Arc method expect:

  • X1, X2, Y1, Y2
  • StartAngle,
  • Extent

As per the itextsharp documentation, Arc draws a partial ellipse inscribed within the rectangle x1,y1,x2,y2 starting (counter-clockwise) at StartAngle degrees and covering extent degrees. I.e. startAng=0 and extent=180 yield an openside-down semi-circle inscribed in the rectangle.

My question is: How to "map" the values in the SvgArcSegment created from the SVG A command into the arguments that PdfContentByte.Arc method expects. I know that the Start and End values are indeed the origin and target of the curve I want, but no clue what RadiusX and RadiusY mean.

1
Like this perhaps?Robert Longson

1 Answers

0
votes

As @RobertLongson pointed in his comment, what I needed was to convert from Center to Endpoint Parametrization.

I'm posting my own C# implementation of the algorithm documented in the SVG documentation, just in case someone else needs it.

        public static SvgCenterParameters EndPointToCenterParametrization(Svg.Pathing.SvgArcSegment arc)
        {
            //// Conversion from endpoint to center parameterization as in SVG Implementation Notes:
            //// https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter

            var sinA = Math.Sin(arc.Angle);
            var cosA = Math.Cos(arc.Angle);

            //// Large arc flag
            var fA = arc.Size == Svg.Pathing.SvgArcSize.Large ? 1 : 0;

            //// Sweep flag
            var fS = arc.Sweep == Svg.Pathing.SvgArcSweep.Positive ? 1 : 0;

            var radiusX = arc.RadiusX;
            var radiusY = arc.RadiusY;
            var x1 = arc.Start.X;
            var y1 = arc.Start.Y;
            var x2 = arc.End.X;
            var y2 = arc.End.Y;

            /*
             *
             * Step 1: Compute (x1′, y1′)
             * 
             */

            //// Median between Start and End
            var midPointX = (x1 - x2) / 2;
            var midPointY = (y1 - y2) / 2;

            var x1p = (cosA * midPointX) + (sinA * midPointY);
            var y1p = (cosA * midPointY) - (sinA * midPointX);

            /*
             *
             * Step 2: Compute (cx′, cy′)
             * 
             */

            var rxry_2 = Math.Pow(radiusX, 2) * Math.Pow(radiusY, 2);
            var rxy1p_2 = Math.Pow(radiusX, 2) * Math.Pow(y1p, 2);
            var ryx1p_2 = Math.Pow(radiusY, 2) * Math.Pow(x1p, 2);
 
            var sqrt = Math.Sqrt(Math.Abs(rxry_2 - rxy1p_2 - ryx1p_2) / (rxy1p_2 + ryx1p_2));
            if (fA == fS)
            {
                sqrt = -sqrt;
            }

            var cXP = sqrt * (radiusX * y1p / radiusY);
            var cYP = sqrt * -(radiusY * x1p / radiusX);

            /*
             *
             * Step 3: Compute (cx, cy) from (cx′, cy′)
             * 
             */

            var cX = (cosA * cXP) - (sinA * cYP) + ((x1 + x2) / 2);
            var cY = (sinA * cXP) + (cosA * cYP) + ((y1 + y2) / 2);

            /*
             *
             * Step 4: Compute θ1 and Δθ
             * 
             */

            var x1pcxp_rx = (float)(x1p - cXP) / radiusX;
            var y1pcyp_ry = (float)(y1p - cYP) / radiusY;
            Vector2 vector1 = new Vector2(1f, 0f);
            Vector2 vector2 = new Vector2(x1pcxp_rx, y1pcyp_ry);

            var angle = Math.Acos(((vector1.x * vector2.x) + (vector1.y * vector2.y)) / (Math.Sqrt((vector1.x * vector1.x) + (vector1.y * vector1.y)) * Math.Sqrt((vector2.x * vector2.x) + (vector2.y * vector2.y)))) * (180 / Math.PI);

            if (((vector1.x * vector2.y) - (vector1.y * vector2.x)) < 0)
            {
                angle = angle * -1;
            }

            var vector3 = new Vector2(x1pcxp_rx, y1pcyp_ry);
            var vector4 = new Vector2((float)(-x1p - cXP) / radiusX, (float)(-y1p - cYP) / radiusY);

            var extent = (Math.Acos(((vector3.x * vector4.x) + (vector3.y * vector4.y)) / Math.Sqrt((vector3.x * vector3.x) + (vector3.y * vector3.y)) * Math.Sqrt((vector4.x * vector4.x) + (vector4.y * vector4.y))) * (180 / Math.PI)) % 360;

            if (((vector3.x * vector4.y) - (vector3.y * vector4.x)) < 0)
            {
                extent = extent * -1;
            }

            if (fS == 1 && extent < 0)
            {
                extent = extent + 360;
            }

            if (fS == 0 && extent > 0)
            {
                extent = extent - 360;
            }

            var rectLL_X = cX - radiusX;
            var rectLL_Y = cY - radiusY;

            var rectUR_X = cX + radiusX;
            var rectUR_Y = cY + radiusY;

            return new SvgCenterParameters
            {
                LlX = (float)rectLL_X,
                LlY = (float)rectLL_Y,
                UrX = (float)rectUR_X,
                UrY = (float)rectUR_Y,
                Angle = (float)angle,
                Extent = (float)extent
            };
        }