OpenPDF的学习文档(第三章·字体样式和插入图片)

字体样式

  • 根据PDF规范,在所有的PDF阅读器和编辑器中(包括浏览器Chrome,Edge等等),都内置了14种字体。
  • 当使用这14种字体时,不用将字体嵌入到PDF中
  • 运行代码可以查看有哪几种字体家族和具体的字体名称。
  • // 获取已注册的字体家族
    Set<String> registeredFamilies = FontFactory.getRegisteredFamilies();
    log.info("registeredFamilies:{}", registeredFamilies);
    // 获取已注册的字体名称
    Set<String> registeredFonts = FontFactory.getRegisteredFonts();
    log.info("registeredFonts:{}", registeredFonts);
    
  • 五大类5字体家族,共14种字体:
    • helvetica 无衬线字体 适合标题和现代排版
      • helvetica(常规)
      • helvetica-bold(粗体)
      • helvetica-oblique(斜体)
      • helvetica-boldoblique(粗斜体)
    • courier 等宽字体 展示代码或模拟打字机效果
      • courier(常规)
      • courier-bold(粗体)
      • courier-oblique(斜体)
      • courier-boldoblique(粗斜体)
    • times-roman 衬线字体 经典正文排版
      • times-roman(常规)
      • times-bold(粗体)
      • times-italic(斜体)
      • times-bolditalic(粗斜体)
    • symbol 数学与希腊字母符号字体
    • zapfdingbats 图标与装饰符号字体
  • 在OpenPDF中,time家族和times-roman家族是同一个家族。
  • 在OpenPDF中,还另外拓展出了两种字体样式:Font.UNDERLINE带下划线和Font.STRIKETHRU带删除线。
  • @Test
    public void font() throws IOException {
        Document document = new Document();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        PdfWriter.getInstance(document, outputStream);
        document.open();
        // 创建通用的分割线对象
        Paragraph linePara = new Paragraph();
        LineSeparator lineSeparator = new LineSeparator(1f, 100f, Color.GRAY, Element.ALIGN_CENTER, 0f);
        // linePara.setSpacingBefore(1f); // 线上方间距
        linePara.setSpacingAfter(20f);  // 线下方间距
        linePara.add(lineSeparator);
    
        // 测试文本
        String text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
        String greece= "αβγδεζηθικλμνξοπρςστυφχψωΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΣΤΥΦΧΨΩ1234567890 My Name Is J.C";
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.NORMAL, Color.BLACK))); // 常规
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.NORMAL, Color.BLACK))); // 常规
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.NORMAL, Color.BLACK))); // 常规
        document.add(linePara);
    
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.BOLD, Color.BLACK))); // 粗体
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.BOLD, Color.BLACK))); // 粗体
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.BOLD, Color.BLACK))); // 粗体
        document.add(linePara);
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.ITALIC, Color.BLACK))); // 斜体
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.ITALIC, Color.BLACK))); // 斜体
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.ITALIC, Color.BLACK))); // 斜体
        document.add(linePara);
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.BOLDITALIC, Color.BLACK))); // 粗斜体
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.BOLDITALIC, Color.BLACK))); // 粗斜体
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.BOLDITALIC, Color.BLACK))); // 粗斜体
        document.add(linePara);
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.UNDERLINE, Color.BLACK))); // 带下划线
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.UNDERLINE, Color.BLACK))); // 带下划线
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.UNDERLINE, Color.BLACK))); // 带下划线
        document.add(linePara);
    
        document.add(new Paragraph(text, new Font(Font.HELVETICA, 12, Font.STRIKETHRU, Color.BLACK)));// 带删除线
        document.add(new Paragraph(text, new Font(Font.COURIER, 12, Font.STRIKETHRU, Color.BLACK)));// 带删除线
        document.add(new Paragraph(text, new Font(Font.TIMES_ROMAN, 12, Font.STRIKETHRU, Color.BLACK)));// 带删除线
        document.add(linePara);
    
        // 这里是坑点,要想使用symbol字体不能这样写
        // 虽然能显示,但是是以其他类型的字体渲染到PDF中的。
        document.add(new Paragraph(greece, new Font(Font.SYMBOL, 12, Font.NORMAL, Color.BLACK)));
        // 得这样写
        document.add(new Paragraph("abg", new Font(Font.SYMBOL, 12, Font.NORMAL, Color.BLACK)));
        document.add(new Paragraph("abg", new Font(Font.SYMBOL, 12, Font.UNDERLINE, Color.BLACK)));
        document.add(new Paragraph("abg", new Font(Font.SYMBOL, 12, Font.STRIKETHRU, Color.BLACK)));
    
        Font helvetica = new Font(Font.HELVETICA, 12, Font.NORMAL, Color.BLACK);
        document.add(SymbolsUtils.createPhraseWithSymbols(greece, helvetica));
    
    
        // 同样的,使用zapfdingbats字体也不能直接写符号,只能写对应的映射
        document.add(new Paragraph("1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", new Font(Font.ZAPFDINGBATS, 12, Font.NORMAL, Color.BLACK)));
    
    
    
        // 在同一个自然段中写入不同字体的文字
        Paragraph paragraph = new Paragraph();
        paragraph.add(new Chunk("HELVETICA", new Font(Font.HELVETICA)));
        paragraph.add(new Chunk("COURIER", new Font(Font.COURIER)));
        paragraph.add(new Chunk("TIMES_ROMAN", new Font(Font.TIMES_ROMAN)));
        document.add(paragraph);
    
        // FontSelector
        document.close();
        Files.write(Paths.get("C:/TMP/font.pdf"), outputStream.toByteArray());
    }
    
  • 查看代码发现有一个坑点,如果想用symbol字体,针对希腊文字αβγδεζ不能直接写αβγδεζ
    • 方法一: 找到symbol字体 希腊字母映射在英文字母对应的表。比如英文字母abg对应αβγ
    • 方法二: 循环使用SpecialSymbol.get(c, defaultFont)方法,如果是希腊字母转换为对应的英文字母,否则使用defaultFont渲染文字。
  • symbol字体映射表
  • zapfdingbats字体映射表
  • 还需要ASCII码表,比如想输出✌,查看映射表发现是32+12=44,在ASCII码表中找到44对应的是逗号,
  • document.add(new Paragraph(",", new Font(Font.ZAPFDINGBATS, 12, Font.NORMAL, Color.BLACK)));
  • image
  • 当然也可以在一个自然段中写入不同字体,不同字号,不同样式,不同颜色的文字,使用Chunk即可。
  • FontSelector是一个很现代化的类,它的作用是根据注册完成的字体,根据顺序来渲染文字。
    • 注意!!! 在使用FontSelectoraddFont()方法时,一定要注意先后顺序!!!
    • // 伪代码
      FontSelector fontSelector = new FontSelector();
      fontSelector.addFont(new Font("英文字体A"));
      fontSelector.addFont(new Font("日文字体B"));
      fontSelector.addFont(new Font("中文字体C"));
      
      Phrase phrase = fontSelector.process("Hello World 你好世界 こんにちは世界");
      paragraph.add(phrase);
      
    • 这里先注册的英文字体A,后注册的日文字体B,最后注册的中文字体C。当OpenPDF渲染这个文本时。会对文本一个字符一个字符进行尝试。
    • H能不能用英文字体A渲染?能就继续下一个字符e,不能则看看能不能用日文字体B渲染;以此类推。
    • 当所有注册的字体都不能渲染上去,FontSelector 有一个托底注册的字体。FontFactory.register("font-fallback/LiberationSans-Regular.ttf", "sans");
    • 当托底注册的字体也不能正常渲染,那么这个字符就不再渲染。

文字排版(竖向)

  • // 书写方式为竖向
    Font font = FontFactory.getFont("STSong-Light", "UniGB-UCS2-V", BaseFont.NOT_EMBEDDED, 14);
    Document document = new Document();
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    PdfWriter writer = PdfWriter.getInstance(document, outputStream);
    document.open();
    
    // 关键点
    PdfContentByte cb = writer.getDirectContent();
    VerticalTextUtils v = new VerticalTextUtils(cb, 500f, 700f, 300f, 5, 24f);
    v.setPhrase(new Phrase("白日依山尽,黄河入海流。", font));
    v.setPhrase(new Phrase("欲穷千里目,更上一层楼。", font));
    
    document.close();
    Files.write(Paths.get("C:/TMP/verticalText.pdf"), outputStream.toByteArray());
    
  • public class VerticalTextUtils {
    
      private final VerticalText vt;
    
      /**
       * 组装 VerticalText 的必要参数
       * @param cb PdfContentByte
       * @param startX 从纸张的左下角算起的磅数(横向)
       * @param startY 从纸张的左下角算起的磅数(竖向)
       * @param height 竖向文本从上到下最大高度
       * @param maxLines 竖向文本最大列数
       * @param leading 列间距
       */
      public VerticalTextUtils(PdfContentByte cb, float startX, float startY, float height, int maxLines, float leading) {
          vt = new VerticalText(cb);
          vt.setVerticalLayout(startX, startY, height, maxLines, leading);
      }
    
      /**
       * 竖向文本(字体需要支持竖向,并且字体的 encoding 属性要设置为 {@link BaseFont#IDENTITY_V})
       * @param phrase Phrase
       */
      public void setPhrase(Phrase phrase) {
          this.vt.addText(phrase);
          this.vt.go();
      }
    
    }
    
  • image

文字连写

这个内容还有bug,没有验证完毕。

字体注册

  • 我们先看看OpenPDF自带的东西。
  • 打开org.openpdf.text.pdf.fonts这个包,我们能看到许多的.afm文件.cmap文件和.properties文件
    • .afm:代表PDF规范的14种字体。
    • .properties:代表字体的高矮胖瘦,字体度量数据。
      • cjkfonts.properties文件规定了 支持的中日韩字体(字体家族)和编码(书写方式
      • 举例说明
        # STSong-Light 是字体家族
        # Adobe-GB1-UCS2_UniGB-UCS2-H_UniGB-UCS2-V_ 是书写方式,使用下划线分隔
        
        # ST = 华文 SinoType、Song = 宋体、Light=细体
        
        # Adobe-GB1-UCS2 = 使用中国国家标准(GB2312/GB18030)字符集
        # UniGB-UCS2-H = 横向书写
        # UniGB-UCS2-V = 竖向书写
        STSong-Light=Adobe-GB1-UCS2_UniGB-UCS2-H_UniGB-UCS2-V_
        # MSung-Light = 蒙纳宋体
        # Adobe-CNS1-UCS2 = 台湾/香港繁体字符集(Big5 扩展)
        # UniCNS-UCS2-H = 横向书写
        # UniCNS-UCS2-V = 竖向书写
        MSung-Light=Adobe-CNS1-UCS2_UniCNS-UCS2-H_UniCNS-UCS2-V_
        
      • 所以知道了字体家族书写方式,我们就可以创建Font字体对象了。
      • // 如果出现乱码,排版错误,则替换为BaseFont.EMBEDDED
        Font font = FontFactory.getFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED, 14);
        
      • 如果阅读器没有自带CJK字体,会请求操作系统;如果操作系统也没有,就会有豆腐块的出现。
    • 所以可以使用上文的FontSelector,来构建一个能输出CJK + 英文 + 希腊字母 + 符号标签的程序。
    • 如果OpenPDF自带的字体都不喜欢,有以下解决办法:
      • <!-- 安装 OpenPDF Fonts Extras -->
        <dependency>
            <groupId>com.github.librepdf</groupId>
            <artifactId>openpdf-fonts-extra</artifactId>
            <version>3.0.5</version>
        </dependency>
        
      • 它包含了Liberation家族的LiberationMono等宽、LiberationSans无衬线、LiberationSerif有衬线
      • Font font = Liberation.MONO.create(10);
        
      • // 不要轻易使用,建议先看看源码。
        FontFactory.registerDirectories();
        
      • 在网上下载其他字体,比如思源字体,Google Noto字体等等。
      • BaseFont bf = BaseFont.createFont(
        "fonts/SourceHanSansSC-Regular.ttf", 
        BaseFont.IDENTITY_V, // 竖排 CMap
        BaseFont.EMBEDDED    // 完整嵌入字体
        );
        
        Font font = new Font(bf, 14);
        
    • .cmap:主要是由Adobe对应的字符标识符CID(Character ID)
      • 在OpenPDF中,需要计算文本的换行、文字在页面上的坐标位置以及水平/竖向推进量。就是豆腐块的大小。

插入图片

  • OpenPDF支持插入jpggifpngbmpwmftif这些类型的图片
  • gif的图片只会截取第一帧,不会播放动画。
  • document.add(new Paragraph("A picture of my dog: otsoe.jpg"));
    Image jpg = Image.getInstanceFromClasspath("images/otsoe.jpg");
    document.add(jpg);
    document.add(new Paragraph("getacro.gif"));
    Image gif = Image.getInstanceFromClasspath("images/getacro.gif");
    document.add(gif);
    document.add(new Paragraph("git.gif"));
    Image git = Image.getInstanceFromClasspath("images/git.gif");
    git.scaleToFit(80f, 80f);
    document.add(git);
    document.add(new Paragraph("pngnow.png"));
    Image png = Image.getInstanceFromClasspath("images/pngnow.png");
    document.add(png);
    document.add(new Paragraph("grayscaled.png"));
    Image grayscaledPng = Image.getInstanceFromClasspath("images/grayscaled.png");
    document.add(grayscaledPng);
    document.add(new Paragraph("iText.bmp"));
    Image bmp = Image.getInstanceFromClasspath("images/iText.bmp");
    document.add(bmp);
    // 声称支持,但是渲染不出来
    document.add(new Paragraph("iText.wmf"));
    Image wmf = Image.getInstanceFromClasspath("images/iText.wmf");
    document.add(wmf);
    document.add(new Paragraph("iText.tif"));
    Image tif = Image.getInstanceFromClasspath("images/iText.tif");
    document.add(tif);
    
  • 图片缩放
    • scalePercent(float percent) 根据百分比来缩放图片
    • scaleToFit(float fitWidth, float fitHeight) 按照比例压缩图片
    • scaleAbsolute(float newWidth, float newHeight)强制指定图片尺寸
  • 对齐方式
    • setAlignment(int alignment) 默认是左对齐
    • Image.ALIGN_LEFT 左对齐
    • Image.ALIGN_CENTER 居中
    • Image.ALIGN_RIGHT 右对齐
    • document.add(new Paragraph("Alignment: left"));
      Image git = Image.getInstanceFromClasspath("images/git.gif");
      git.scalePercent(30f);
      git.setAlignment(Image.ALIGN_LEFT);
      document.add(git);
      document.add(new Paragraph("Alignment: center"));
      git.setAlignment(Image.ALIGN_CENTER);
      document.add(git);
      document.add(new Paragraph("Alignment: right"));
      git.setAlignment(Image.ALIGN_RIGHT);
      document.add(git);
      
    • image
    • Image.TEXTWRAP 文字环绕方式,组合使用:image.setAlignment(Image.LEFT | Image.TEXTWRAP);
      • document.add(new Paragraph("Alignment: left and textwrap"));
        Image git = Image.getInstanceFromClasspath("images/git.gif");
        git.setAlignment(Image.ALIGN_LEFT | Image.TEXTWRAP);
        document.add(git);
        document.add(new Paragraph(longText));
        document.add(new Paragraph("Alignment: right and textwrap"));
        git.setAlignment(Image.ALIGN_RIGHT | Image.TEXTWRAP);
        document.add(git);
        document.add(new Paragraph(longText));
        
      • image
    • Image.UNDERLYING 将图片置于底层,组合使用:image.setAlignment(Image.RIGHT | Image.UNDERLYING);
      • document.add(new Paragraph("Alignment: left and underlying"));
        git.setAlignment(Image.ALIGN_CENTER | Image.UNDERLYING);
        document.add(git);
        document.add(new Paragraph(longText));
        document.add(new Paragraph("Alignment: right and underlying"));
        git.setAlignment(Image.ALIGN_RIGHT | Image.UNDERLYING);
        document.add(git);
        document.add(new Paragraph(longText));
        
      • image
  • 二维码与条形码
  • 这里只介绍BarCode128QrCode,其它类型的条形码和二维码大同小异(关键是看业务用哪个)。
  • OpenPDF原生支持BarCode128,不用引入其他包。
    • /**
       * 创建 BarCode128 条形码
       * @param writer writer
       * @param code 条形码内容
       * @return Image
       */
      public static Image barcode128(PdfWriter writer, String code){
          Barcode qrCode = new Barcode128();
          qrCode.setCode(code);
          // 第一个Color.BLACK是条形码的颜色
          // 第二个Color.BLACK是条形码下文字的颜色
          return qrCode.createImageWithBarcode(writer.getDirectContent(), Color.BLACK, Color.BLACK);
      }
      
  • QrCode需要引入第三方包
    • 这里使用Google的zxing来示例
    • <dependency>
          <groupId>com.google.zxing</groupId>
          <artifactId>javase</artifactId>
          <version>3.5.4</version>
      </dependency>
      
    • // 设置关键属性
      private static final Map<EncodeHintType, Object> HINTS = new HashMap<>();
      {
          {
              HINTS.put(EncodeHintType.CHARACTER_SET, "UTF-8"); // 设置字符编码
              HINTS.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H); // 设置为30%的纠错方式
              HINTS.put(EncodeHintType.MARGIN, 1); // 二维码边距
          }
      }
      /**
       * 生成二维码
       * @param content 二维码内容
       * @param size 大小(像素)
       * @return BufferedImage
       */
      public BufferedImage createQRCode(String content, int size) throws WriterException {
          QRCodeWriter writer = new QRCodeWriter();
          BitMatrix matrix = writer.encode(
                  content,
                  BarcodeFormat.QR_CODE,
                  size,
                  size,
                  HINTS
          );
          return MatrixToImageWriter.toBufferedImage(matrix);
      }
      /**
       * 生成二维码(中心带图片)
       * @param content 二维码内容
       * @param size 像素
       * @param logoStream logo 图像
       * @return BufferedImage
       */
      public BufferedImage createQRCode(String content, int size, InputStream logoStream) throws WriterException, IOException {
          QRCodeWriter writer = new QRCodeWriter();
          BitMatrix matrix = writer.encode(
                  content,
                  BarcodeFormat.QR_CODE,
                  size,
                  size,
                  HINTS
          );
          // 转换为 BufferedImage
          BufferedImage qrImage = MatrixToImageWriter.toBufferedImage(matrix);
          // 读取图片
          BufferedImage logo = ImageIO.read(logoStream);
      
          int qrWidth = qrImage.getWidth();
          int qrHeight = qrImage.getHeight();
      
          // 设置 Logo 尺寸:约为二维码的 1/5 到 1/4
          int logoSize = Math.min(qrWidth, qrHeight) / 3;
          int logoX = (qrWidth - logoSize) / 2;
          int logoY = (qrHeight - logoSize) / 2;
      
          // 创建新图像(避免修改原图)
          BufferedImage combined = new BufferedImage(qrWidth, qrHeight, BufferedImage.TYPE_INT_ARGB);
          Graphics2D g = combined.createGraphics();
      
          // 先画二维码
          g.drawImage(qrImage, 0, 0, null);
      
          // 再画缩放后的 Logo(保持比例)
          g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
          g.drawImage(logo, logoX, logoY, logoSize, logoSize, null);
      
          g.dispose();
      
          return combined;
      }
      
posted @ 2026-07-28 12:41  铁锅炖大鹅ing  阅读(1)  评论(0)    收藏  举报