You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PDFBox中如何设置多边形描边为内部/外部模式?

在PDFBox 2.0.25中实现多边形的内部/外部描边

PDFBox的fillAndStroke()方法默认采用居中描边(描边宽度一半在路径内,一半在路径外),没有提供像JavaFXStrokeType那样直接指定描边位置的API。要实现内部或外部描边,需要通过路径偏移手动处理,核心思路是生成原路径向内/向外偏移后的路径,再分别执行填充和描边操作。

解决方案原理

  • 内部描边:先填充原多边形路径,再对原路径向内偏移描边宽度/2的距离,描边这个偏移后的路径,确保描边完全落在原路径内部。
  • 外部描边:先对原路径向外偏移描边宽度/2的距离并描边,再填充原路径,覆盖内部多余的描边部分,确保描边完全落在原路径外部。

代码实现

工具类:路径偏移与描边绘制

import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.graphics.PDPath;
import java.awt.geom.Point2D;
import java.io.IOException;

public class StrokeUtils {

    // 生成偏移后的路径:offset为正向外偏移,负向内偏移(仅支持凸多边形)
    public static PDPath createOffsetPath(PDPath originalPath, float offset) throws IOException {
        java.util.List<Point2D.Float> points = new java.util.ArrayList<>();
        originalPath.visit(pathSegment -> {
            float[] coords = pathSegment.getCoordinates();
            if (pathSegment.getSegmentType() == PDPath.PathSegment.MOVETO || 
                pathSegment.getSegmentType() == PDPath.PathSegment.LINETO) {
                points.add(new Point2D.Float(coords[0], coords[1]));
            }
            return true;
        });
        if (points.isEmpty()) return originalPath;

        java.util.List<Point2D.Float> offsetPoints = new java.util.ArrayList<>();
        int n = points.size();
        for (int i = 0; i < n; i++) {
            Point2D.Float p0 = points.get(i);
            Point2D.Float p1 = points.get((i+1)%n);
            Point2D.Float p2 = points.get((i+2)%n);

            // 计算边的法向量(顺时针顶点的左侧法向量为向内方向)
            Point2D.Float v1 = new Point2D.Float(p1.x - p0.x, p1.y - p0.y);
            float len1 = (float) Math.hypot(v1.x, v1.y);
            Point2D.Float n1 = new Point2D.Float(-v1.y / len1, v1.x / len1);

            Point2D.Float v2 = new Point2D.Float(p2.x - p1.x, p2.y - p1.y);
            float len2 = (float) Math.hypot(v2.x, v2.y);
            Point2D.Float n2 = new Point2D.Float(-v2.y / len2, v2.x / len2);

            // 平均法向量,计算偏移后的顶点
            Point2D.Float avgN = new Point2D.Float(n1.x + n2.x, n1.y + n2.y);
            float avgLen = (float) Math.hypot(avgN.x, avgN.y);
            avgN.x /= avgLen;
            avgN.y /= avgLen;

            offsetPoints.add(new Point2D.Float(p1.x + offset * avgN.x, p1.y + offset * avgN.y));
        }

        PDPath offsetPath = new PDPath();
        if (!offsetPoints.isEmpty()) {
            offsetPath.moveTo(offsetPoints.get(0).x, offsetPoints.get(0).y);
            for (int i = 1; i < offsetPoints.size(); i++) {
                offsetPath.lineTo(offsetPoints.get(i).x, offsetPoints.get(i).y);
            }
            offsetPath.closePath();
        }
        return offsetPath;
    }

    // 绘制带内部描边的多边形
    public static void drawInsideStrokePolygon(PDPageContentStream contentStream, float[] points, 
                                              float fillR, float fillG, float fillB,
                                              float strokeR, float strokeG, float strokeB,
                                              float strokeWidth) throws IOException {
        // 创建原路径
        PDPath originalPath = new PDPath();
        originalPath.moveTo(points[0], points[1]);
        for (int i = 2; i < points.length; i += 2) {
            originalPath.lineTo(points[i], points[i+1]);
        }
        originalPath.closePath();

        // 填充原路径
        contentStream.setNonStrokingColor(new PDColor(new float[]{fillR, fillG, fillB}, PDDeviceRGB.INSTANCE));
        contentStream.fill(originalPath);

        // 生成向内偏移的路径并描边
        PDPath offsetPath = createOffsetPath(originalPath, -strokeWidth / 2);
        contentStream.setStrokingColor(new PDColor(new float[]{strokeR, strokeG, strokeB}, PDDeviceRGB.INSTANCE));
        contentStream.setLineWidth(strokeWidth);
        contentStream.stroke(offsetPath);
    }

    // 绘制带外部描边的多边形
    public static void drawOutsideStrokePolygon(PDPageContentStream contentStream, float[] points, 
                                               float fillR, float fillG, float fillB,
                                               float strokeR, float strokeG, float strokeB,
                                               float strokeWidth) throws IOException {
        // 创建原路径
        PDPath originalPath = new PDPath();
        originalPath.moveTo(points[0], points[1]);
        for (int i = 2; i < points.length; i += 2) {
            originalPath.lineTo(points[i], points[i+1]);
        }
        originalPath.closePath();

        // 生成向外偏移的路径并描边
        PDPath offsetPath = createOffsetPath(originalPath, strokeWidth / 2);
        contentStream.setStrokingColor(new PDColor(new float[]{strokeR, strokeG, strokeB}, PDDeviceRGB.INSTANCE));
        contentStream.setLineWidth(strokeWidth);
        contentStream.stroke(offsetPath);

        // 填充原路径,覆盖内部多余描边
        contentStream.setNonStrokingColor(new PDColor(new float[]{fillR, fillG, fillB}, PDDeviceRGB.INSTANCE));
        contentStream.fill(originalPath);
    }
}

使用示例(对应JavaFX示例)

// 多边形顶点坐标,与JavaFX示例一致
float[] polygonPoints = {50.0f, 150.0f, 150.0f, 50.0f, 250.0f, 150.0f, 150.0f, 250.0f};

// 绘制内部描边的多边形(对应JavaFX的StrokeType.INSIDE)
StrokeUtils.drawInsideStrokePolygon(
    contentStream,
    polygonPoints,
    0.8f, 0.93f, 1.0f, // 填充色:浅蓝色
    0.0f, 0.0f, 0.0f, // 描边色:黑色
    3.0f // 描边宽度
);

注意事项

  • 上述路径偏移方法仅针对凸多边形有效,如果需要处理凹多边形,建议借助Java2D的Area类或第三方几何库实现更复杂的路径偏移逻辑。
  • 偏移量必须设置为描边宽度/2,才能保证描边完全落在原路径的内部或外部。

内容的提问来源于stack exchange,提问作者Guyard

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 06:00:54