View Javadoc
1   package fr.tiogars.chat.domains.task.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.chat.domains.task.models.Task;
18  
19  /** Renders printable Task documents with wrapping and A4 pagination. */
20  @Component
21  public class TaskPdfRenderer {
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<Task> templates) throws IOException {
38          try (PDDocument document = new PDDocument();
39                  InputStream fontStream = TaskPdfRenderer.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 Tasks.");
50                  }
51                  for (Task 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("Status", template.status());
58                      writer.field("Priority", template.priority());
59                      writer.field("Due date", template.dueDate());
60                      writer.field("Assigned to", template.assignedTo());
61                      writer.field("Tag IDs", template.tagIds());
62                      writer.field("Nomenclature ID", template.nomenclatureId());
63                      writer.field("Created", template.createdDate());
64                      writer.field("Created by", template.createdBy());
65                      writer.field("Last modified", template.lastModifiedDate());
66                      writer.field("Last modified by", template.lastModifiedBy());
67                  }
68              }
69              document.save(output);
70              return output.toByteArray();
71          }
72      }
73  
74      private static final class PageWriter implements AutoCloseable {
75  
76          private final PDDocument document;
77          private final PDType0Font font;
78          private final String title;
79          private PDPageContentStream stream;
80          private float y;
81  
82          private PageWriter(PDDocument document, PDType0Font font, String title) {
83              this.document = document;
84              this.font = font;
85              this.title = title;
86          }
87  
88          private void newPage() throws IOException {
89              close();
90              PDPage page = new PDPage(PDRectangle.A4);
91              document.addPage(page);
92              stream = new PDPageContentStream(document, page);
93              text(title, 18, page.getMediaBox().getHeight() - MARGIN);
94              text("Page " + document.getNumberOfPages(), 9, MARGIN / 2);
95              y = page.getMediaBox().getHeight() - MARGIN - 36;
96          }
97  
98          private void field(String label, Object value) throws IOException {
99              String content = label + ": " + (value == null ? "Not available" : value);
100             for (String paragraph : content.replace("\t", "    ").split("\\R", -1)) {
101                 StringBuilder line = new StringBuilder();
102                 float width = 0;
103                 for (int codePoint : paragraph.codePoints().toArray()) {
104                     String character = new String(Character.toChars(codePoint));
105                     float characterWidth = characterWidth(character);
106                     if (width + characterWidth > TEXT_WIDTH && !line.isEmpty()) {
107                         String currentLine = line.toString();
108                         int wordBreak = currentLine.lastIndexOf(' ');
109                         line(wordBreak > 0 ? currentLine.substring(0, wordBreak) : currentLine);
110                         line.setLength(0);
111                         if (wordBreak > 0) {
112                             line.append(currentLine.substring(wordBreak + 1));
113                         }
114                         width = font.getStringWidth(line.toString()) * FONT_SIZE / 1000;
115                     }
116                     line.append(character);
117                     width += characterWidth;
118                 }
119                 line(line.toString());
120             }
121             y -= LINE_HEIGHT / 2;
122         }
123 
124         private float characterWidth(String character) throws IOException {
125             try {
126                 return font.getStringWidth(character) * FONT_SIZE / 1000;
127             } catch (IllegalArgumentException exception) {
128                 throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_CONTENT,
129                         "A Task contains characters unsupported by the PDF font.", exception);
130             }
131         }
132 
133         private void line(String value) throws IOException {
134             if (y < MARGIN) {
135                 newPage();
136             }
137             text(value, FONT_SIZE, y);
138             y -= LINE_HEIGHT;
139         }
140 
141         private void text(String value, float size, float baseline) throws IOException {
142             stream.beginText();
143             stream.setFont(font, size);
144             stream.newLineAtOffset(MARGIN, baseline);
145             stream.showText(value);
146             stream.endText();
147         }
148 
149         @Override
150         public void close() throws IOException {
151             if (stream != null) {
152                 stream.close();
153                 stream = null;
154             }
155         }
156     }
157 }