diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java index fd9c2678..86d22da2 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java +++ b/docs-core/src/main/java/com/sismics/docs/core/util/PdfUtil.java @@ -7,6 +7,8 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; import javax.imageio.ImageIO; @@ -17,6 +19,7 @@ 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.PDType1Font; import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; @@ -29,8 +32,11 @@ import org.odftoolkit.odfdom.doc.OdfTextDocument; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; import com.google.common.io.Closer; +import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.jpa.File; +import com.sismics.docs.core.util.pdf.PdfPage; import com.sismics.util.ImageUtil; import com.sismics.util.mime.MimeType; @@ -141,21 +147,16 @@ public class PdfUtil { /** * Convert a document and its files to a merged PDF file. * + * @param documentDto Document DTO * @param fileList List of files * @param fitImageToPage Fit images to the page * @param metadata Add a page with metadata - * @param comment Add a page with comments * @param margin Margins in millimeters * @return PDF input stream * @throws IOException */ - public static InputStream convertToPdf(List fileList, boolean fitImageToPage, boolean metadata, boolean comments, int margin) throws Exception { - // TODO PDF Export: Option to add a front page with: - // document title, document description, creator, date created, language, - // additional dublincore metadata (except relations) - // list of all files (and information if it is in this document or not) - // TODO PDF Export: Option to add the comments - + public static InputStream convertToPdf(DocumentDto documentDto, List fileList, + boolean fitImageToPage, boolean metadata, int margin) throws Exception { // Setup PDFBox Closer closer = Closer.create(); MemoryUsageSetting memUsageSettings = MemoryUsageSetting.setupMixed(1000000); // 1MB max memory usage @@ -166,12 +167,45 @@ public class PdfUtil { try (PDDocument doc = new PDDocument(memUsageSettings)) { // Add metadata if (metadata) { - - } - - // Add comments - if (comments) { - + PDPage page = new PDPage(); + doc.addPage(page); + try (PdfPage pdfPage = new PdfPage(doc, page, margin * mmPerInch, PDType1Font.HELVETICA, 12)) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + pdfPage.addText(documentDto.getTitle(), true, PDType1Font.HELVETICA_BOLD, 16) + .newLine() + .addText("Created by " + documentDto.getCreator() + + " on " + dateFormat.format(new Date(documentDto.getCreateTimestamp())), true) + .newLine() + .addText(documentDto.getDescription()) + .newLine(); + if (!Strings.isNullOrEmpty(documentDto.getSubject())) { + pdfPage.addText("Subject: " + documentDto.getSubject()); + } + if (!Strings.isNullOrEmpty(documentDto.getIdentifier())) { + pdfPage.addText("Identifier: " + documentDto.getIdentifier()); + } + if (!Strings.isNullOrEmpty(documentDto.getPublisher())) { + pdfPage.addText("Publisher: " + documentDto.getPublisher()); + } + if (!Strings.isNullOrEmpty(documentDto.getFormat())) { + pdfPage.addText("Format: " + documentDto.getFormat()); + } + if (!Strings.isNullOrEmpty(documentDto.getSource())) { + pdfPage.addText("Source: " + documentDto.getSource()); + } + if (!Strings.isNullOrEmpty(documentDto.getType())) { + pdfPage.addText("Type: " + documentDto.getType()); + } + if (!Strings.isNullOrEmpty(documentDto.getCoverage())) { + pdfPage.addText("Coverage: " + documentDto.getCoverage()); + } + if (!Strings.isNullOrEmpty(documentDto.getRights())) { + pdfPage.addText("Rights: " + documentDto.getRights()); + } + pdfPage.addText("Language: " + documentDto.getLanguage()) + .newLine() + .addText("Files in this document : " + fileList.size(), false, PDType1Font.HELVETICA_BOLD, 12); + } } // Add files diff --git a/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java new file mode 100644 index 00000000..af9aafb2 --- /dev/null +++ b/docs-core/src/main/java/com/sismics/docs/core/util/pdf/PdfPage.java @@ -0,0 +1,153 @@ +package com.sismics.docs.core.util.pdf; + +import java.io.Closeable; +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; + +/** + * Wrapper around PDFBox for high level abstraction of PDF writing. + * + * @author bgamard + */ +public class PdfPage implements Closeable { + private PDPage pdPage; + private PDPageContentStream pdContent; + private float margin; + private PDFont defaultFont; + private int defaultFontSize; + + /** + * Create a wrapper around a PDF page. + * + * @param pdDoc Document + * @param pdPage Page + * @param margin Margin + * @param defaultFont Default font + * @param defaultFontSize Default fond size + * @throws IOException + */ + public PdfPage(PDDocument pdDoc, PDPage pdPage, float margin, PDFont defaultFont, int defaultFontSize) throws IOException { + this.pdPage = pdPage; + this.pdContent = new PDPageContentStream(pdDoc, pdPage); + this.margin = margin; + this.defaultFont = defaultFont; + this.defaultFontSize = defaultFontSize; + + pdContent.beginText(); + pdContent.newLineAtOffset(margin, pdPage.getMediaBox().getHeight() - margin); + } + + /** + * Write a text with default font. + * + * @param text Text + * @throws IOException + */ + public PdfPage addText(String text) throws IOException { + drawText(pdPage.getMediaBox().getWidth() - 2 * margin, defaultFont, defaultFontSize, text, false); + return this; + } + + /** + * Write a text with default font. + * + * @param text Text + * @param centered If true, the text will be centered in the page + * @throws IOException + */ + public PdfPage addText(String text, boolean centered) throws IOException { + drawText(pdPage.getMediaBox().getWidth() - 2 * margin, defaultFont, defaultFontSize, text, centered); + return this; + } + + /** + * Write a text in the page. + * + * @param text Text + * @param centered If true, the text will be centered in the page + * @param font Font + * @param fontSize Font size + * @throws IOException + */ + public PdfPage addText(String text, boolean centered, PDFont font, int fontSize) throws IOException { + drawText(pdPage.getMediaBox().getWidth() - 2 * margin, font, fontSize, text, centered); + return this; + } + + /** + * Create a new line. + * + * @throws IOException + */ + public PdfPage newLine() throws IOException { + pdContent.newLineAtOffset(0, - defaultFont.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * defaultFontSize); + return this; + } + + /** + * Draw a text with low level PDFBox API. + * + * @param paragraphWidth Paragraph width + * @param font Font + * @param fontSize Font size + * @param text Text + * @param centered If true, the text will be centered in the paragraph + * @throws IOException + */ + private void drawText(float paragraphWidth, PDFont font, int fontSize, String text, boolean centered) throws IOException { + pdContent.setFont(font, fontSize); + int start = 0; + int end = 0; + float height = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize; + for (int i : possibleWrapPoints(text)) { + float width = font.getStringWidth(text.substring(start, i)) / 1000 * fontSize; + if (start < end && width > paragraphWidth) { + // Draw partial text and increase height + pdContent.newLineAtOffset(0, - height); + String line = text.substring(start, end); + float lineWidth = font.getStringWidth(line) / 1000 * fontSize; + float offset = (paragraphWidth - lineWidth) / 2; + if (centered) pdContent.newLineAtOffset(offset, 0); + pdContent.showText(line); + if (centered) pdContent.newLineAtOffset(- offset, 0); + start = end; + } + end = i; + } + + // Last piece of text + String line = text.substring(start); + float lineWidth = font.getStringWidth(line) / 1000 * fontSize; + float offset = (paragraphWidth - lineWidth) / 2; + pdContent.newLineAtOffset(0, - height); + if (centered) pdContent.newLineAtOffset(offset, 0); + pdContent.showText(line); + if (centered) pdContent.newLineAtOffset(- offset, 0); + } + + /** + * Returns wrap points for a given piece of text. + * + * @param text Text + * @return Wrap points + */ + private int[] possibleWrapPoints(String text) { + String[] split = text.split("(?<=\\W)"); + int[] ret = new int[split.length]; + ret[0] = split[0].length(); + for (int i = 1 ; i < split.length ; i++) { + ret[i] = ret[i-1] + split[i].length(); + } + return ret; + } + + @Override + public void close() throws IOException { + pdContent.endText(); + pdContent.close(); + } +} diff --git a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java index c768d4cb..c12ac1a4 100644 --- a/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java +++ b/docs-core/src/test/java/com/sismics/docs/core/util/TestFileUtil.java @@ -1,18 +1,22 @@ package com.sismics.docs.core.util; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; +import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Date; +import org.junit.Assert; import org.junit.Test; import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; import com.google.common.io.Resources; +import com.sismics.docs.core.dao.jpa.dto.DocumentDto; import com.sismics.docs.core.model.jpa.File; import com.sismics.util.mime.MimeType; -import org.junit.Assert; - /** * Test of the file entity utilities. * @@ -50,6 +54,21 @@ public class TestFileUtil { InputStream inputStream2 = Resources.getResource("file/udhr_encrypted.pdf").openStream(); InputStream inputStream3 = Resources.getResource("file/document.docx").openStream(); InputStream inputStream4 = Resources.getResource("file/document.odt").openStream()) { + // Document + DocumentDto documentDto = new DocumentDto(); + documentDto.setTitle("My super document 1"); + documentDto.setDescription("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis id turpis iaculis, commodo est ac, efficitur quam. Nam accumsan magna in orci vulputate ultricies. Sed vulputate neque magna, at laoreet leo ultricies vel. Proin eu hendrerit felis. Quisque sit amet arcu efficitur, pulvinar orci sed, imperdiet elit. Nunc posuere ex sed fermentum congue. Aliquam ultrices convallis finibus. Praesent iaculis justo vitae dictum auctor. Praesent suscipit imperdiet erat ac maximus. Aenean pharetra quam sed fermentum commodo. Donec sagittis ipsum nibh, id congue dolor venenatis quis. In tincidunt nisl non ex sollicitudin, a imperdiet neque scelerisque. Nullam lacinia ac orci sed faucibus. Donec tincidunt venenatis justo, nec fermentum justo rutrum a."); + documentDto.setSubject("A set of random picture"); + documentDto.setIdentifier("ID-2016-08-00001"); + documentDto.setPublisher("My Publisher, Inc."); + documentDto.setFormat("A4 standard ISO format"); + documentDto.setType("Image"); + documentDto.setCoverage("France"); + documentDto.setRights("Public Domain"); + documentDto.setLanguage("en"); + documentDto.setCreator("user1"); + documentDto.setCreateTimestamp(new Date().getTime()); + // First file Files.copy(inputStream0, DirectoryUtil.getStorageDirectory().resolve("apollo_landscape"), StandardCopyOption.REPLACE_EXISTING); File file0 = new File(); @@ -81,7 +100,10 @@ public class TestFileUtil { file4.setId("document_odt"); file4.setMimeType(MimeType.OPEN_DOCUMENT_TEXT); - PdfUtil.convertToPdf(Lists.newArrayList(file0, file1, file2, file3, file4), true, true, true, 10).close(); + try (InputStream pdfInputStream = PdfUtil.convertToPdf(documentDto, Lists.newArrayList(file0, file1, file2, file3, file4), true, true, 10); + OutputStream fileOutputStream = Files.newOutputStream(Paths.get("c:/temp.pdf"))) { + ByteStreams.copy(pdfInputStream, fileOutputStream); + } } } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java index 41313b55..558be97b 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/DocumentResource.java @@ -206,7 +206,7 @@ public class DocumentResource extends BaseResource { // Get document and check read permission DocumentDao documentDao = new DocumentDao(); - DocumentDto documentDto = documentDao.getDocument(documentId, PermType.READ, shareId == null ? principal.getId() : shareId); + final DocumentDto documentDto = documentDao.getDocument(documentId, PermType.READ, shareId == null ? principal.getId() : shareId); if (documentDto == null) { return Response.status(Status.NOT_FOUND).build(); } @@ -226,7 +226,7 @@ public class DocumentResource extends BaseResource { StreamingOutput stream = new StreamingOutput() { @Override public void write(OutputStream outputStream) throws IOException, WebApplicationException { - try (InputStream inputStream = PdfUtil.convertToPdf(fileList, fitImageToPage, metadata, comments, margin)) { + try (InputStream inputStream = PdfUtil.convertToPdf(documentDto, fileList, fitImageToPage, metadata, margin)) { ByteStreams.copy(inputStream, outputStream); } catch (Exception e) { throw new IOException(e);