英文:
Drawing Curvy Lines Through PDFBox
问题
使用PDFBox,我已经创建了一个线图来绘制一些数据,它看起来很像你会在谷歌搜索中看到的一般线图。它也与我附在这个问题中的线图完全相同。线图绘制算法的工作方式是查看当前点,然后查看下一个点,如果找到有效的点,就会绘制一条线。
我的问题是客户不喜欢线条之间连接得太锐利。相反,他们希望线条之间的连接以更曲线的方式进行。附上了客户想要的大致想法的图像。请注意,虽然线条看起来非常弯曲,但客户特别关心线条连接本身的曲线,而不是像标准线图中那样尖锐。
到目前为止,我尝试过使用贝塞尔曲线,但似乎找不到使它适合所有不同点之间的幅度的正确值。我首先尝试更改线条末端和连接样式,但这没有产生期望的线条连接之间的"曲线"效果。我还考虑过使用路径来实现这个结果,但我还没有找到如何继续的方法。
是否有什么我忽略了可以使绘制这些线条变得更容易的东西?如果没有,是否有人可以帮助我找出正确的贝塞尔值/路径以实现这些曲线?提前感谢任何建议/代码示例。
由于签署了保密协议,我无法提供显示如何绘制和绘制图表的代码示例(这将完全暴露我们的算法)。我只能说的是,我为数据在图表中的绘制创建了一个内部表示,这个系统在提供的图像中非常粗略地呈现。我可以说的是,绘制数据的函数仅使用PDPageContentStream类的lineTo和strokeTo函数,以及对基于我们内部坐标表示的起始点的初始moveTo。
英文:
Using PDFBox, I have created a line chart to plot some data, and it looks much like any general line chart you will see through a google search. It also looks identical to the line chart I've attached to this question. The way the line chart drawing algorithm works is that it looks at the current point, and then the next one, and a line is drawn if a valid point is found there.
My problem is that a client does not like how sharply the lines connect with each other. Instead, they want the joins between the lines to happen in more of a curved fashion. Attached is an image of a rough idea of what the client wants. Note that although the lines look very curvy, the client specifically cares about the line joins themselves being curvy, and not sharp like in a standard line chart.
So far, I have tried using Bézier curves, but I can't seem to find the right values to make it scale right for all the different magnitudes between the points. I first tried changing the line cap and line join styles, but this did not produce the desired "cuvyness" between the line joins. I have also contemplated using paths to achieve this result, but I haven't managed to figure out how to proceed.
Is there something that I'm missing that could make it easier to draw these lines? If not, can anyone help me figure out the right Bézier values/paths to achieve these curves? Thanks in advance for any suggestions/code examples.
Due to an NDA, I cannot give a code example that shows how the chart is drawn and plotted (this would give up our algorithm entirely). All that I can say is that I created an internal representation for how the data should be plotted in the chart, and this system is very roughly translated in the provided image. I can say that the function that plots the data exclusively uses the PDPageContentStream classes's lineTo and strokeTo functions, after an initial moveTo to the position of the starting point based on our internal coordinate representation.
答案1
得分: 2
以下是翻译好的部分:
"解决方案"是使用圆形线连接而不是尖角连接(默认情况下) - 似乎我漏掉了这一点。
您示例中的图表可能使用曲线插值,这个问题和答案可能会对您有所帮助:https://stackoverflow.com/questions/17425763/how-does-polyline-simplification-in-adobe-illustrator-work
以下是将一系列线转换为贝塞尔连接线的代码(它是C#,但可以进行最小更改以转换为Java):
/// <summary>
/// 在页面上绘制贝塞尔连接线。
/// </summary>
/// <param name="page">要绘制线条的页面。</param>
/// <param name="points">表示连接线的点列表。</param>
/// <param name="pen">用于绘制最终路径的画笔。</param>
/// <param name="smoothFactor">用于计算贝塞尔曲线的平滑因子。</param>
/// <param name="font"></param>
private static void DrawBezierConnectedLines(PDFPage page, PDFPoint[] points, PDFPen pen, double smoothFactor, PDFFont font)
{
// 省略部分代码...
}
/// <summary>
/// 给定表示2条连接线的3个连续点序列,该方法计算显示新线和连接曲线所需的点。
/// </summary>
/// <param name="pt1">第一个点</param>
/// <param name="pt2">第二个点</param>
/// <param name="pt3">第三个点</param>
/// <param name="smoothFactor">用于计算贝塞尔曲线的平滑因子</param>
/// <param name="isFirstSection">如果这些点是点列表中的前3个点,则为True</param>
/// <param name="isLastSection">如果这3个点是点列表中的最后3个点,则为True</param>
/// <returns>表示新线和连接曲线的点列表。</returns>
/// <remarks>如果这是第一部分,则方法返回5个点,表示第一条线、连接曲线和最后一条线。
/// 如果这不是第一部分,则方法返回4个点,表示连接曲线和最后一条线。</remarks>
private static PDFPoint[] ComputeBezierConnectedLines(PDFPoint pt1, PDFPoint pt2, PDFPoint pt3, double smoothFactor, bool isFirstSection, bool isLastSection)
{
// 省略部分代码...
}
// 省略部分代码...
对于这组点:
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
英文:
---A quick "solution" is to use round line joins instead of miter joins (the default) --- it seems that I missed this.
The charts in your sample probably use curve interpolation and this question and answers might help you: https://stackoverflow.com/questions/17425763/how-does-polyline-simplification-in-adobe-illustrator-work
The code below shows how to transform a list of lines into Bezier connected lines (it's C# but it can be converted to Java with minimal changes):
/// <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;
}
For this set of points:
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);
The corresponding PDF file can be downloaded here:
https://github.com/o2solutions/pdf4net/blob/master/GettingStarted/BezierConnectedLines/BezierConnectedLines.pdf
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论