blob: d207fe8878db3a17e9f11ebe3b9b9db02694222d [file] [log] [blame]
/*
*******************************************************************************
* Copyright (c) 2020 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************
*/
package org.eclipse.openk.statementpublicaffairs.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.PostConstruct;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.eclipse.openk.statementpublicaffairs.exceptions.BadRequestServiceException;
import org.eclipse.openk.statementpublicaffairs.exceptions.InternalErrorServiceException;
import org.eclipse.openk.statementpublicaffairs.model.AttachmentFile;
import org.eclipse.openk.statementpublicaffairs.model.CompanyContactBlockModel;
import org.eclipse.openk.statementpublicaffairs.model.FontModification;
import org.eclipse.openk.statementpublicaffairs.model.PageContext;
import org.eclipse.openk.statementpublicaffairs.model.TemplateClosingConfig;
import org.eclipse.openk.statementpublicaffairs.model.TemplateClosingConfigSignature;
import org.eclipse.openk.statementpublicaffairs.model.TemplateConfig;
import org.eclipse.openk.statementpublicaffairs.model.TemplateTextBlockConfig;
import org.eclipse.openk.statementpublicaffairs.model.TextState;
import org.eclipse.openk.statementpublicaffairs.model.TextToken;
import org.eclipse.openk.statementpublicaffairs.model.Textblock;
import org.eclipse.openk.statementpublicaffairs.service.compile.TextCompileUtil;
import org.eclipse.openk.statementpublicaffairs.service.compile.Token;
import org.eclipse.openk.statementpublicaffairs.viewmodel.TextConfiguration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
@Service
public class StatementCompileService {
private static final String TEXT_NL = "\n";
private static final String TEXT_BULLET = " • ";
private static final String TEXT_SPACE = " ";
private boolean drawRects = false;
public static final double DPI = 72.0;
public static final double CM_PER_INCH = 2.54;
@Value("${statement.compile.dateFormatPattern:\"dd.MM.yyyy\"}")
private String dateFormatPattern;
@Value("${statement.compile.statementFileName:\"Statement.pdf\"}")
private String statementFileName = "Statement.pdf";
@Value("${statement.compile.templatePdf}")
private String templatePdfPath;
@Value("${statement.compile.templateJson}")
private String templateJsonPath;
private Map<FontModification, PDFont> fonts;
@PostConstruct
public void init() {
fonts = new HashMap<>();
fonts.put(FontModification.Normal, PDType1Font.HELVETICA);
fonts.put(FontModification.Bold, PDType1Font.HELVETICA_BOLD);
fonts.put(FontModification.Italic, PDType1Font.HELVETICA_OBLIQUE);
}
public AttachmentFile generatePDF(TextConfiguration textConfiguration, CompanyContactBlockModel contact,
List<Textblock> textArrangement) throws InternalErrorServiceException, BadRequestServiceException {
List<List<TextToken>> textBlockSets = TextCompileUtil.convertToTextTokens(textConfiguration, textArrangement);
return printPdf(textBlockSets, textConfiguration, contact);
}
private PDDocument getTemplatePDF() throws InternalErrorServiceException {
try {
File templateFile = ResourceUtils.getFile(templatePdfPath);
InputStream templateStream = new FileInputStream(templateFile);
return PDDocument.load(templateStream);
} catch (IOException e) {
throw new InternalErrorServiceException("Could not load templatePdf from file " + templatePdfPath, e);
}
}
private TemplateConfig getTemplateConfig() throws InternalErrorServiceException {
try {
File file = ResourceUtils.getFile(templateJsonPath);
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(file, TemplateConfig.class);
} catch (IOException e) {
throw new InternalErrorServiceException("Could not load templateConfig from file " + templateJsonPath, e);
}
}
private AttachmentFile printPdf(List<List<TextToken>> textblockSets, TextConfiguration textConfiguration,
CompanyContactBlockModel contact) throws InternalErrorServiceException, BadRequestServiceException {
AttachmentFile attachmentFile = new AttachmentFile();
PDPage firstPageTemplate = null;
PDPage followupPageTemplate = null;
TemplateConfig templateConfig = getTemplateConfig();
PDDocument sourcedoc = getTemplatePDF();
firstPageTemplate = sourcedoc.getPage(0);
followupPageTemplate = sourcedoc.getPage(1);
String welcomeText = String.join(TEXT_NL, templateConfig.getContentP1().getText());
welcomeText = TextCompileUtil.fillPlaceholder(welcomeText, new HashMap<String, String>(),
textConfiguration.getReplacements(), textConfiguration.getConfiguration().getSelects());
List<TextToken> welcomeTokens = TextCompileUtil.parseTextToToken(welcomeText);
textblockSets.add(0, welcomeTokens);
PDDocument document = new PDDocument();
PDDocumentInformation info = document.getDocumentInformation();
info.setTitle("Statement-" + textConfiguration.getReplacements().get("id"));
PDPage firstPage = clonePage(firstPageTemplate);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
drawAddressBox(document, firstPage, templateConfig, contact);
drawInfoBox(document, firstPage, templateConfig, textConfiguration.getReplacements());
drawContent(document, firstPage, followupPageTemplate, templateConfig, textblockSets);
document.save(out);
} catch (IOException e) {
try {
document.close();
} catch (IOException e1) {
//
}
throw new InternalErrorServiceException("Exception when drawing the statement pdf.", e);
}
byte[] data = out.toByteArray();
InputStream is = new ByteArrayInputStream(data);
attachmentFile.setRessource(is);
attachmentFile.setName(statementFileName);
attachmentFile.setType("application/pdf");
attachmentFile.setLength(out.size());
return attachmentFile;
}
protected float dpiOfCm(double cm) {
return (float) (cm / CM_PER_INCH * DPI);
}
private float calcYPosFromTopCm(double cm, float dpiHeight) {
return dpiHeight - dpiOfCm(cm);
}
private float calcSize(PDFont font, float fontSize, String text) throws IOException {
return fontSize * font.getStringWidth(text) / 1000;
}
private List<String> cutintopieces(PDFont font, float fontSize, String text, double width) throws IOException {
String[] token = text.split(TEXT_SPACE);
List<String> lines = new ArrayList<>();
int tokenIndex = 0;
List<String> line = new ArrayList<>();
for (tokenIndex = 0; tokenIndex < token.length; tokenIndex++) {
line.add(token[tokenIndex]);
if (calcSize(font, fontSize, String.join(TEXT_SPACE, line)) > width) {
String tmp = line.remove(line.size() - 1);
lines.add(String.join(TEXT_SPACE, line));
line.clear();
line.add(tmp);
}
}
if (!line.isEmpty()) {
lines.add(String.join(TEXT_SPACE, line));
}
return lines;
}
private void drawRect(PageContext context, PDPageContentStream cont) throws IOException {
if (drawRects) {
cont.addRect(context.getPosX(), (float) (context.getPosY() + context.getLeading() - context.getHeight()),
context.getWidth(), context.getHeight());
cont.setLineWidth(1);
cont.stroke();
}
}
private void drawContent(PDDocument document, PDPage firstPage, PDPage followupPageTemplate,
TemplateConfig templateConfig, List<List<TextToken>> textblockSets) throws IOException, BadRequestServiceException {
List<PDPage> pages = new ArrayList<>();
boolean first = true;
List<List<TextToken>> remainingTextSets = new ArrayList<>(textblockSets);
PDPage currentPage;
int contextLines;
PageContext context;
int pagescount = 0;
int maxPagesCount = textblockSets.size();
do {
TemplateTextBlockConfig cfg;
if (first) {
currentPage = firstPage;
cfg = templateConfig.getContentP1();
first = false;
} else {
currentPage = clonePage(followupPageTemplate);
cfg = templateConfig.getContentP2();
}
document.addPage(currentPage);
pages.add(currentPage);
PDPageContentStream cont = new PDPageContentStream(document, currentPage, AppendMode.APPEND, false, true);
context = new PageContext();
context.setPageHeight(currentPage.getMediaBox().getHeight());
context.setPosX(dpiOfCm(cfg.getX()));
context.setPosY(calcYPosFromTopCm(cfg.getY(), context.getPageHeight()));
context.setWidth(dpiOfCm(cfg.getWidth()));
context.setHeight(dpiOfCm(cfg.getHeight()));
context.setLeading(cfg.getFontSize() * 1.12);
context.setFontSize(templateConfig.getContentP1().getFontSize());
drawRect(context, cont);
contextLines = drawContentPage(remainingTextSets, cont, context);
pagescount++;
if (pagescount > maxPagesCount) {
throw new BadRequestServiceException("Error, page could not be printed. Maybe a textblock is too large to fit on one page");
}
} while (!remainingTextSets.isEmpty());
TemplateClosingConfig closingConf = templateConfig.getClosing();
double height = closingConf.getSalutation().size() * context.getLeading();
height += dpiOfCm(closingConf.getSignatures().get(0).getYOffset());
height += (closingConf.getSignatures().get(0).getLines().size() + 2) * context.getLeading();
float relYCursor = (float) (contextLines * context.getLeading());
float posYCursor = context.getPosY() - relYCursor;
PageContext closingContext = new PageContext();
if ((context.getHeight() - relYCursor) < height) {
currentPage = clonePage(followupPageTemplate);
TemplateTextBlockConfig cfg = templateConfig.getContentP2();
document.addPage(currentPage);
pages.add(currentPage);
closingContext.setPageHeight(currentPage.getMediaBox().getHeight());
closingContext.setPosX(dpiOfCm(cfg.getX()));
closingContext.setPosY(calcYPosFromTopCm(cfg.getY(), closingContext.getPageHeight()));
closingContext.setWidth(dpiOfCm(cfg.getWidth()));
closingContext.setHeight((float) height);
closingContext.setLeading(cfg.getFontSize() * 1.12);
closingContext.setFontSize(templateConfig.getContentP1().getFontSize());
} else {
closingContext.setPageHeight(context.getPageHeight());
closingContext.setPosX(context.getPosX());
closingContext.setPosY(posYCursor);
closingContext.setWidth(context.getWidth());
closingContext.setHeight((float) height);
closingContext.setLeading(context.getLeading());
closingContext.setFontSize(context.getFontSize());
}
PDPageContentStream cont = new PDPageContentStream(document, currentPage, AppendMode.APPEND, false, true);
drawClosing(closingContext, cont, templateConfig.getClosing());
}
private void drawClosing(PageContext context, PDPageContentStream cont, TemplateClosingConfig closingConfig)
throws IOException {
drawRect(context, cont);
cont.beginText();
cont.setFont(fonts.get(FontModification.Normal), context.getFontSize());
cont.setLeading(context.getLeading());
cont.newLineAtOffset(context.getPosX(), context.getPosY());
for (String line : closingConfig.getSalutation()) {
cont.showText(line);
cont.newLine();
}
float posYCursor = (float) (context.getPosY() - closingConfig.getSalutation().size() * context.getLeading());
cont.endText();
for (TemplateClosingConfigSignature signature : closingConfig.getSignatures()) {
float posY = posYCursor - dpiOfCm(signature.getYOffset());
float posX = context.getPosX() + dpiOfCm(signature.getXOffset());
cont.moveTo(posX, posY);
cont.lineTo(posX + dpiOfCm(signature.getWidth()), posY);
cont.setLineWidth(0.3f);
cont.stroke();
cont.beginText();
cont.setFont(fonts.get(FontModification.Normal), context.getFontSize());
cont.setLeading(context.getLeading());
cont.newLineAtOffset(posX, posY);
cont.newLine();
for (String signatureLine : signature.getLines()) {
cont.showText(signatureLine);
cont.newLine();
}
cont.endText();
}
cont.close();
}
private int drawContentPage(List<List<TextToken>> remainingTextSets, PDPageContentStream cont, PageContext context)
throws IOException {
cont.beginText();
cont.setLeading(context.getLeading());
cont.newLineAtOffset(context.getPosX(), context.getPosY());
DrawContentContext drawContext = new DrawContentContext();
int resultLines = 0;
do {
List<TextToken> set = new ArrayList<>(remainingTextSets.get(0));
DrawContentContext resultContext = drawTokenList(drawContext, set, context.getWidth(),
context.getMaxLines(), context.getFontSize(), cont);
resultLines = resultContext.lines;
if (resultContext.fitOnPage && !resultContext.pagebreak) {
remainingTextSets.remove(0);
drawContext = resultContext;
} else {
if (resultContext.fitOnPage && resultContext.pagebreak) {
remainingTextSets.remove(0);
}
break;
}
} while (!remainingTextSets.isEmpty());
cont.endText();
cont.close();
return resultLines;
}
private DrawContentContext drawTokenList(DrawContentContext context, List<TextToken> set, double maxWidth,
int maxLines, float fontSize, PDPageContentStream cont) throws IOException {
DrawContentContext resContext = new DrawContentContext();
FontModification lastLineFontMode = FontModification.Normal;
double currentTextWidth = context.currentTextWidth;
// if specialtype
if (set.size() == 1 && set.get(0).getType() == Token.TK_PB) {
resContext.pagebreak = true;
resContext.fitOnPage = true;
return resContext;
}
List<List<TextToken>> lines = new ArrayList<>();
List<TextToken> line = new ArrayList<>();
if (currentTextWidth > 0) {
line.add(TextToken.newSpaceTextToken());
line.add(TextToken.newSpaceTextToken());
}
int linecount = 0;
while (!set.isEmpty()) {
TextToken token = set.remove(0);
TextState newState = calculateLineWidth(lastLineFontMode, currentTextWidth, line, token, fontSize);
resContext.currentTextWidth = newState.getWidth();
if (newState.isLineBreak()) {
linecount++;
line.add(token);
lines.add(line);
line = new ArrayList<>();
resContext.currentTextWidth = 0;
currentTextWidth = 0;
lastLineFontMode = newState.getFontMode();
} else if (newState.getWidth() <= maxWidth) {
line.add(token);
} else {
linecount++;
line.add(TextToken.newLineTextToken());
if (newState.isInBulletPoint()) {
line.add(TextToken.newBulletShiftTextToken());
}
lines.add(line);
line = new ArrayList<>();
line.add(token);
currentTextWidth = 0;
resContext.currentTextWidth = 0;
lastLineFontMode = newState.getFontMode();
}
}
lines.add(line);
if (maxLines >= (context.lines + linecount)) {
drawTextSet(lines, cont, fontSize);
resContext.lines = context.lines + linecount;
resContext.fitOnPage = true;
} else {
resContext.pagebreak = true;
}
return resContext;
}
private class DrawContentContext {
private int lines;
private double currentTextWidth;
private boolean fitOnPage;
private boolean pagebreak;
}
private FontModification updateFontMod(FontModification currentFontMod, Token token) {
FontModification newFontMod;
switch (token) {
case TK_BOLD:
if (currentFontMod == FontModification.Bold) {
newFontMod = FontModification.Normal;
} else {
newFontMod = FontModification.Bold;
}
break;
case TK_ITALIC:
if (currentFontMod == FontModification.Italic) {
newFontMod = FontModification.Normal;
} else {
newFontMod = FontModification.Italic;
}
break;
default:
newFontMod = FontModification.Normal;
}
return newFontMod;
}
private void drawTextSet(List<List<TextToken>> lines, PDPageContentStream cont, float fontSize) throws IOException {
FontModification currentFontMod = FontModification.Normal;
boolean inBulletPoint = false;
for (List<TextToken> line : lines) {
boolean firstInLine = true;
for (TextToken lt : line) {
switch (lt.getType()) {
case TK_NL:
cont.newLine();
inBulletPoint = false;
break;
case TK_BOLD:
case TK_ITALIC:
currentFontMod = updateFontMod(currentFontMod, lt.getType());
break;
case TK_BULLET:
cont.setFont(fonts.get(currentFontMod), fontSize);
cont.showText(TEXT_BULLET);
inBulletPoint = true;
break;
case TK_SPACE:
if (!firstInLine) {
cont.setFont(fonts.get(currentFontMod), fontSize);
cont.showText(TEXT_SPACE);
}
break;
case TK_UNDERSCORE:
case STRING:
cont.setFont(fonts.get(currentFontMod), fontSize);
if (firstInLine && inBulletPoint) {
cont.showText(" ");
}
cont.showText(lt.getValue());
break;
default:
break;
}
firstInLine = false;
}
}
}
private TextState calculateLineWidth(FontModification initialFontMod, double currentTextWidth, List<TextToken> line,
TextToken token, float fontSize) throws IOException {
List<TextToken> tokens = new ArrayList<>(line);
tokens.add(token);
double width = currentTextWidth;
FontModification currentFontMod = initialFontMod;
TextState result = new TextState();
for (TextToken lt : tokens) {
switch (lt.getType()) {
case TK_BOLD:
case TK_ITALIC:
currentFontMod = updateFontMod(currentFontMod, lt.getType());
break;
case TK_SPACE:
width += calcSize(fonts.get(currentFontMod), fontSize, TEXT_SPACE);
break;
case TK_BULLET:
width += calcSize(fonts.get(currentFontMod), fontSize, TEXT_BULLET);
result.setInBulletPoint(true);
break;
case STRING:
width += calcSize(fonts.get(currentFontMod), fontSize, lt.getValue());
break;
case TK_NL:
result.setLineBreak(true);
result.setInBulletPoint(false);
break;
default:
break;
}
}
result.setWidth(width);
result.setFontMode(currentFontMod);
return result;
}
private void drawInfoBox(PDDocument document, PDPage firstPage, TemplateConfig templateConfig, Map<String, String> replacements) throws IOException {
PDPageContentStream cont = new PDPageContentStream(document, firstPage, AppendMode.APPEND, false, true);
float pageHeight = firstPage.getMediaBox().getHeight();
TemplateTextBlockConfig cfg = templateConfig.getInfo();
float posX = dpiOfCm(cfg.getX());
float posY = calcYPosFromTopCm(cfg.getY(), pageHeight);
float width = dpiOfCm(cfg.getWidth());
float height = dpiOfCm(cfg.getHeight());
List<String> text = cfg.getText();
List<String> lines = new ArrayList<>();
for (String textEntry : text) {
textEntry = TextCompileUtil.fillPlaceholder(textEntry, null, replacements, new HashMap<>());
lines.addAll(cutintopieces(fonts.get(FontModification.Normal), cfg.getFontSize(), textEntry, width));
}
double leading = cfg.getFontSize() * 1.12;
PageContext context = new PageContext();
context.setPosX(posX);
context.setPosY(posY);
context.setLeading(leading);
context.setWidth(width);
context.setHeight(height);
drawRect(context, cont);
cont.beginText();
cont.setFont(fonts.get(FontModification.Normal), cfg.getFontSize());
cont.setLeading(leading);
cont.newLineAtOffset(posX, posY);
for (String line : lines) {
cont.showText(line);
cont.newLine();
}
cont.close();
}
private void drawAddressBox(PDDocument document, PDPage firstPage, TemplateConfig templateConfig,
CompanyContactBlockModel contact) throws IOException {
List<String> address = new ArrayList<>();
address.add(contact.getCompany());
List<String> nameEntries = new ArrayList<>();
if (contact.getSalutation() != null) {
nameEntries.add(contact.getSalutation());
}
if (contact.getTitle() != null) {
nameEntries.add(contact.getTitle());
}
nameEntries.add(contact.getFirstName());
nameEntries.add(contact.getLastName());
address.add(String.join(TEXT_SPACE, nameEntries));
address.add(contact.getStreet() + contact.getHouseNumber());
address.add(contact.getPostCode() + TEXT_SPACE + contact.getCommunity());
if (contact.getCommunitySuffix() != null) {
address.add(contact.getCommunitySuffix());
}
PDPageContentStream cont = new PDPageContentStream(document, firstPage, AppendMode.APPEND, false, true);
float pageHeight = firstPage.getMediaBox().getHeight();
TemplateTextBlockConfig addressCfg = templateConfig.getAddress();
float posX = dpiOfCm(addressCfg.getX());
float posY = calcYPosFromTopCm(addressCfg.getY(), pageHeight);
float width = dpiOfCm(addressCfg.getWidth());
float height = dpiOfCm(addressCfg.getHeight());
List<String> lines = new ArrayList<>();
for (String textEntry : address) {
lines.addAll(cutintopieces(fonts.get(FontModification.Normal), addressCfg.getFontSize(), textEntry, width));
}
double leading = addressCfg.getFontSize() * 1.12;
PageContext context = new PageContext();
context.setPosX(posX);
context.setPosY(posY);
context.setLeading(leading);
context.setWidth(width);
context.setHeight(height);
drawRect(context, cont);
cont.beginText();
cont.setFont(fonts.get(FontModification.Normal), addressCfg.getFontSize());
cont.setLeading(leading);
cont.newLineAtOffset(posX, posY);
for (String line : lines) {
cont.showText(line);
cont.newLine();
}
cont.endText();
cont.close();
}
private PDPage clonePage(PDPage original) {
COSDictionary pageDict = original.getCOSObject();
COSDictionary newPageDict = new COSDictionary(pageDict);
newPageDict.removeItem(COSName.ANNOTS);
return new PDPage(newPageDict);
}
}