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
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
31
32
33
34
35
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 }