通过 PDFBox 绘制曲线

Drawing Curvy Lines Through PDFBox

我使用 PDFBox 创建了一个折线图来绘制一些数据,它看起来很像您通过 google 搜索看到的任何一般折线图。它看起来也与我附在这个问题上的折线图相同。折线图绘制算法的工作方式是查看当前点,然后查看下一个点,如果在那里找到有效点,则绘制一条线。

我的问题是客户不喜欢线条之间的连接有多尖锐。相反,他们希望线条之间的连接以更弯曲的方式发生。附件是客户想要什么的粗略想法的图像。请注意,虽然线条看起来非常弯曲,但客户特别关心线条本身是否弯曲,而不是像标准折线图中那样尖锐。

到目前为止,我已经尝试使用贝塞尔曲线,但我似乎无法找到正确的值来使其针对点之间的所有不同幅度进行正确缩放。我首先尝试更改线帽和线连接样式,但这并没有在线连接之间产生所需的“弯曲度”。我也考虑过使用路径来实现这个结果,但我还没有弄清楚如何进行。

有什么我遗漏的东西可以让画这些线更容易吗?如果没有,谁能帮我找出正确的 Bézier values/paths 来实现这些曲线?预先感谢任何 suggestions/code 个示例。

由于保密协议,我无法提供代码示例来说明如何绘制和绘制图表(这将完全放弃我们的算法)。我能说的是,我为数据在图表中的绘制方式创建了一个内部表示,并且这个系统在提供的图像中非常粗略地翻译。我可以说绘制数据的函数专门使用 PDPageContentStream 类 的 lineTo 和 strokeTo 函数,在初始 moveTo 到基于我们内部坐标表示的起点位置之后。

A rough idea of the curves the client wants.

--- 一个快速的“解决方案”是使用圆线连接而不是斜接(默认)--- 看来我错过了这个。

您示例中的图表可能使用曲线插值,这个问题和答案可能对您有所帮助:How does polyline simplification in Adobe Illustrator work?

下面的代码显示了如何将线列表转换为贝塞尔曲线连接线(它是 C#,但可以通过最小的更改转换为 Java):

/// <summary>
/// Draws the Bezier connected lines on the page.
/// </summary>
/// <param name="page">Page where to draw the lines.</param>
/// <param name="points">List of points representing the connected lines.</param>
/// <param name="pen">Pen to draw the final path.</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="font"></param>
private static void DrawBezierConnectedLines(PDFPage page, PDFPoint[] points, PDFPen pen, double smoothFactor, PDFFont font)
{

    PDFPath path = new PDFPath();
    path.StartSubpath(points[0].X, points[0].Y);

    for (int i = 0; i < points.Length - 2; i++)
    {
        PDFPoint[] pts = ComputeBezierConnectedLines(points[i], points[i + 1], points[i + 2], smoothFactor, i == 0, i == points.Length - 3);
        switch (pts.Length)
        {
            case 2: // Intermediate/last section - straight lines
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                break;
            case 3: // First section - straight lines
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                path.AddLineTo(pts[2].X, pts[2].Y);
                break;
            case 4: // Intermediate/last section
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddBezierTo(pts[1].X, pts[1].Y, pts[1].X, pts[1].Y, pts[2].X, pts[2].Y);
                path.AddLineTo(pts[3].X, pts[3].Y);
                break;
            case 5: // First section
                path.AddLineTo(pts[0].X, pts[0].Y);
                path.AddLineTo(pts[1].X, pts[1].Y);
                path.AddBezierTo(pts[2].X, pts[2].Y, pts[2].X, pts[2].Y, pts[3].X, pts[3].Y);
                path.AddLineTo(pts[4].X, pts[4].Y);
                break;
        }
    }

    page.Canvas.DrawPath(pen, path);

    page.Canvas.DrawString($"Smooth factor = {smoothFactor}", font, new PDFBrush(), points[points.Length - 1].X, points[0].Y);
}

/// <summary>
/// Given a sequence of 3 consecutive points representing 2 connected lines the method computes the points required to display the new lines and the connecting curve.
/// </summary>
/// <param name="pt1">First point</param>
/// <param name="pt2">Second point</param>
/// <param name="pt3">Third point</param>
/// <param name="smoothFactor">Smooth factor for computing the Bezier curve</param>
/// <param name="isFirstSection">True if the points are the first 3 in the list of points</param>
/// <param name="isLastSection">True if the 3 points are last 3 in the list of points.</param>
/// <returns>A list of points representing the new lines and the connecting curve.</returns>
/// <remarks>The method returns 5 points if this is the first section, points that represent the first line, connecting curve and last line.
/// If this is not the first section the method returns 4 points representing the connecting curve and the last line.</remarks>
private static PDFPoint[] ComputeBezierConnectedLines(PDFPoint pt1, PDFPoint pt2, PDFPoint pt3, double smoothFactor, bool isFirstSection, bool isLastSection)
{
    PDFPoint[] outputPoints = null;

    if (smoothFactor > 0.5)
    {
        smoothFactor = 0.5; // Half line maximum
    }
    if (((pt1.X == pt2.X) && (pt2.X == pt3.X)) || // Vertical lines
        ((pt1.Y == pt2.Y) && (pt2.Y == pt3.Y)) || // Horizontal lines
        (smoothFactor == 0))
    {
        if (!isFirstSection)
        {
            pt1 = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
        }
        if (!isLastSection)
        {
            pt3 = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
        }
        if (isFirstSection)
        {
            outputPoints = new PDFPoint[] { pt1, pt2, pt3 };
        }
        else
        {
            outputPoints = new PDFPoint[] { pt2, pt3 };
        }
    }
    else
    {
        PDFPoint startPoint = new PDFPoint(pt1);
        if (!isFirstSection)
        {
            startPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, false);
        }
        PDFPoint firstIntermediaryPoint = ComputeIntermediatePoint(pt1, pt2, smoothFactor, true);
        PDFPoint secondIntermediaryPoint = new PDFPoint(pt2);
        PDFPoint thirdIntermediaryPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, false);
        PDFPoint endPoint = new PDFPoint(pt3);
        if (!isLastSection)
        {
            endPoint = ComputeIntermediatePoint(pt2, pt3, smoothFactor, true);
        }

        if (isFirstSection)
        {
            outputPoints = new PDFPoint[] { startPoint, firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
        }
        else
        {
            outputPoints = new PDFPoint[] { firstIntermediaryPoint, secondIntermediaryPoint, thirdIntermediaryPoint, endPoint };
        }
    }

    return outputPoints;
}

/// <summary>
/// Given the line from pt1 to pt2 the method computes an intermediary point on the line.
/// </summary>
/// <param name="pt1">Start point</param>
/// <param name="pt2">End point</param>
/// <param name="smoothFactor">Smooth factor specifying how from from the line end the intermediary point is located.</param>
/// <param name="isEndLocation">True if the intermediary point should be computed relative to end point, 
/// false if the intermediary point should be computed relative to start point.</param>
/// <returns>A point on the line defined by pt1->pt2</returns>
private static PDFPoint ComputeIntermediatePoint(PDFPoint pt1, PDFPoint pt2, double smoothFactor, bool isEndLocation)
{
    if (isEndLocation)
    {
        smoothFactor = 1 - smoothFactor;
    }

    PDFPoint intermediate = new PDFPoint();
    if (pt1.X == pt2.X)
    {
        intermediate.X = pt1.X;
        intermediate.Y = pt1.Y + (pt2.Y - pt1.Y) * smoothFactor;
    }
    else
    {
        intermediate.X = pt1.X + (pt2.X - pt1.X) * smoothFactor;
        intermediate.Y = (intermediate.X * (pt2.Y - pt1.Y) + (pt2.X * pt1.Y - pt1.X * pt2.Y)) / (pt2.X - pt1.X);
    }

    return intermediate;
}

对于这组点数:

PDFPoint[] points = new PDFPoint[] {
    new PDFPoint(50, 150), new PDFPoint(100, 200), new PDFPoint(150, 50), new PDFPoint(200, 150), new PDFPoint(250, 50) };
DrawBezierConnectedLines(page, points, pen, 0, helvetica);

这是结果:

相应的PDF文件可以在这里下载: https://github.com/o2solutions/pdf4net/blob/master/GettingStarted/BezierConnectedLines/BezierConnectedLines.pdf