DomainTemplatePdfRenderer.java
package fr.tiogars.inception.domains.domaintemplate.services;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import fr.tiogars.inception.domains.domaintemplate.models.DomainTemplate;
/** Renders printable DomainTemplate documents with wrapping and A4 pagination. */
@Component
public class DomainTemplatePdfRenderer {
private static final String FONT_RESOURCE = "/org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf";
private static final float MARGIN = 48;
private static final float FONT_SIZE = 11;
private static final float LINE_HEIGHT = 16;
private static final float TEXT_WIDTH = PDRectangle.A4.getWidth() - 2 * MARGIN;
/**
* Renders one record per page, continuing long records on additional pages.
*
* @param title document heading
* @param templates records to print, in display order
* @return PDF bytes
* @throws IOException when the PDF cannot be generated
*/
public byte[] render(String title, List<DomainTemplate> templates) throws IOException {
try (PDDocument document = new PDDocument();
InputStream fontStream = DomainTemplatePdfRenderer.class.getResourceAsStream(FONT_RESOURCE);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
if (fontStream == null) {
throw new IOException("The bundled PDF font is unavailable.");
}
PDType0Font font = PDType0Font.load(document, fontStream);
document.getDocumentInformation().setTitle(title);
try (PageWriter writer = new PageWriter(document, font, title)) {
if (templates.isEmpty()) {
writer.newPage();
writer.field("Records", "No DomainTemplates.");
}
for (DomainTemplate template : templates) {
writer.newPage();
writer.field("ID", template.id());
writer.field("Code", template.code());
writer.field("Label", template.label());
writer.field("Description", template.description());
writer.field("Created", template.createdDate());
writer.field("Created by", template.createdBy());
writer.field("Last modified", template.lastModifiedDate());
writer.field("Last modified by", template.lastModifiedBy());
}
}
document.save(output);
return output.toByteArray();
}
}
private static final class PageWriter implements AutoCloseable {
private final PDDocument document;
private final PDType0Font font;
private final String title;
private PDPageContentStream stream;
private float y;
private PageWriter(PDDocument document, PDType0Font font, String title) {
this.document = document;
this.font = font;
this.title = title;
}
private void newPage() throws IOException {
close();
PDPage page = new PDPage(PDRectangle.A4);
document.addPage(page);
stream = new PDPageContentStream(document, page);
text(title, 18, page.getMediaBox().getHeight() - MARGIN);
text("Page " + document.getNumberOfPages(), 9, MARGIN / 2);
y = page.getMediaBox().getHeight() - MARGIN - 36;
}
private void field(String label, Object value) throws IOException {
String content = label + ": " + (value == null ? "Not available" : value);
for (String paragraph : content.replace("\t", " ").split("\\R", -1)) {
StringBuilder line = new StringBuilder();
float width = 0;
for (int codePoint : paragraph.codePoints().toArray()) {
String character = new String(Character.toChars(codePoint));
float characterWidth = characterWidth(character);
if (width + characterWidth > TEXT_WIDTH && !line.isEmpty()) {
String currentLine = line.toString();
int wordBreak = currentLine.lastIndexOf(' ');
line(wordBreak > 0 ? currentLine.substring(0, wordBreak) : currentLine);
line.setLength(0);
if (wordBreak > 0) {
line.append(currentLine.substring(wordBreak + 1));
}
width = font.getStringWidth(line.toString()) * FONT_SIZE / 1000;
}
line.append(character);
width += characterWidth;
}
line(line.toString());
}
y -= LINE_HEIGHT / 2;
}
private float characterWidth(String character) throws IOException {
try {
return font.getStringWidth(character) * FONT_SIZE / 1000;
} catch (IllegalArgumentException exception) {
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_CONTENT,
"A DomainTemplate contains characters unsupported by the PDF font.", exception);
}
}
private void line(String value) throws IOException {
if (y < MARGIN) {
newPage();
}
text(value, FONT_SIZE, y);
y -= LINE_HEIGHT;
}
private void text(String value, float size, float baseline) throws IOException {
stream.beginText();
stream.setFont(font, size);
stream.newLineAtOffset(MARGIN, baseline);
stream.showText(value);
stream.endText();
}
@Override
public void close() throws IOException {
if (stream != null) {
stream.close();
stream = null;
}
}
}
}