View Javadoc
1   package fr.tiogars.inception.domains.domaintemplate.services;
2   
3   import java.io.ByteArrayOutputStream;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.util.List;
7   
8   import org.apache.pdfbox.pdmodel.PDDocument;
9   import org.apache.pdfbox.pdmodel.PDPage;
10  import org.apache.pdfbox.pdmodel.PDPageContentStream;
11  import org.apache.pdfbox.pdmodel.common.PDRectangle;
12  import org.apache.pdfbox.pdmodel.font.PDType0Font;
13  import org.springframework.http.HttpStatus;
14  import org.springframework.stereotype.Component;
15  import org.springframework.web.server.ResponseStatusException;
16  
17  import fr.tiogars.inception.domains.domaintemplate.models.DomainTemplate;
18  
19  /** Renders printable DomainTemplate documents with wrapping and A4 pagination. */
20  @Component
21  public class DomainTemplatePdfRenderer {
22  
23      private static final String FONT_RESOURCE = "/org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf";
24      private static final float MARGIN = 48;
25      private static final float FONT_SIZE = 11;
26      private static final float LINE_HEIGHT = 16;
27      private static final float TEXT_WIDTH = PDRectangle.A4.getWidth() - 2 * MARGIN;
28  
29      /**
30       * Renders one record per page, continuing long records on additional pages.
31       *
32       * @param title document heading
33       * @param templates records to print, in display order
34       * @return PDF bytes
35       * @throws IOException when the PDF cannot be generated
36       */
37      public byte[] render(String title, List<DomainTemplate> templates) throws IOException {
38          try (PDDocument document = new PDDocument();
39                  InputStream fontStream = DomainTemplatePdfRenderer.class.getResourceAsStream(FONT_RESOURCE);
40                  ByteArrayOutputStream output = new ByteArrayOutputStream()) {
41              if (fontStream == null) {
42                  throw new IOException("The bundled PDF font is unavailable.");
43              }
44              PDType0Font font = PDType0Font.load(document, fontStream);
45              document.getDocumentInformation().setTitle(title);
46              try (PageWriter writer = new PageWriter(document, font, title)) {
47                  if (templates.isEmpty()) {
48                      writer.newPage();
49                      writer.field("Records", "No DomainTemplates.");
50                  }
51                  for (DomainTemplate template : templates) {
52                      writer.newPage();
53                      writer.field("ID", template.id());
54                      writer.field("Code", template.code());
55                      writer.field("Label", template.label());
56                      writer.field("Description", template.description());
57                      writer.field("Created", template.createdDate());
58                      writer.field("Created by", template.createdBy());
59                      writer.field("Last modified", template.lastModifiedDate());
60                      writer.field("Last modified by", template.lastModifiedBy());
61                  }
62              }
63              document.save(output);
64              return output.toByteArray();
65          }
66      }
67  
68      private static final class PageWriter implements AutoCloseable {
69  
70          private final PDDocument document;
71          private final PDType0Font font;
72          private final String title;
73          private PDPageContentStream stream;
74          private float y;
75  
76          private PageWriter(PDDocument document, PDType0Font font, String title) {
77              this.document = document;
78              this.font = font;
79              this.title = title;
80          }
81  
82          private void newPage() throws IOException {
83              close();
84              PDPage page = new PDPage(PDRectangle.A4);
85              document.addPage(page);
86              stream = new PDPageContentStream(document, page);
87              text(title, 18, page.getMediaBox().getHeight() - MARGIN);
88              text("Page " + document.getNumberOfPages(), 9, MARGIN / 2);
89              y = page.getMediaBox().getHeight() - MARGIN - 36;
90          }
91  
92          private void field(String label, Object value) throws IOException {
93              String content = label + ": " + (value == null ? "Not available" : value);
94              for (String paragraph : content.replace("\t", "    ").split("\\R", -1)) {
95                  StringBuilder line = new StringBuilder();
96                  float width = 0;
97                  for (int codePoint : paragraph.codePoints().toArray()) {
98                      String character = new String(Character.toChars(codePoint));
99                      float characterWidth = characterWidth(character);
100                     if (width + characterWidth > TEXT_WIDTH && !line.isEmpty()) {
101                         String currentLine = line.toString();
102                         int wordBreak = currentLine.lastIndexOf(' ');
103                         line(wordBreak > 0 ? currentLine.substring(0, wordBreak) : currentLine);
104                         line.setLength(0);
105                         if (wordBreak > 0) {
106                             line.append(currentLine.substring(wordBreak + 1));
107                         }
108                         width = font.getStringWidth(line.toString()) * FONT_SIZE / 1000;
109                     }
110                     line.append(character);
111                     width += characterWidth;
112                 }
113                 line(line.toString());
114             }
115             y -= LINE_HEIGHT / 2;
116         }
117 
118         private float characterWidth(String character) throws IOException {
119             try {
120                 return font.getStringWidth(character) * FONT_SIZE / 1000;
121             } catch (IllegalArgumentException exception) {
122                 throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_CONTENT,
123                         "A DomainTemplate contains characters unsupported by the PDF font.", exception);
124             }
125         }
126 
127         private void line(String value) throws IOException {
128             if (y < MARGIN) {
129                 newPage();
130             }
131             text(value, FONT_SIZE, y);
132             y -= LINE_HEIGHT;
133         }
134 
135         private void text(String value, float size, float baseline) throws IOException {
136             stream.beginText();
137             stream.setFont(font, size);
138             stream.newLineAtOffset(MARGIN, baseline);
139             stream.showText(value);
140             stream.endText();
141         }
142 
143         @Override
144         public void close() throws IOException {
145             if (stream != null) {
146                 stream.close();
147                 stream = null;
148             }
149         }
150     }
151 }