Hot questions for Using PDFBox in spring boot

Top Java Programmings / PDFBox / spring boot

Question:

My Spring boot Java application is using apache pdf box library {version 2.0.6} for generating pdf. I want decimal values to be right aligned. It means all decimal dot should align in same vertical line. I also attached screenShot.

    stream.beginText();
            stream.newLineAtOffset(xCordinate, yCordinate);
            stream.showText(String.valueOf(item.getQuantity()));
            List<String> resultList = processTextData(TextUtil.isEmpty(item.getDescription()) ? "-" : item.getDescription());
            int y = 0;
            int x = 50;
            int tempYcordinate = yCordinate;
            for (String string : resultList) {
                stream.newLineAtOffset(x, y);
                stream.showText(processStringForPdf(string));
                x = 0;
                y = -8;

            }
            tempYcordinate = tempYcordinate - (8 * resultList.size());
            stream.endText();
            stream.beginText();
            stream.newLineAtOffset(285, yCordinate);
            stream.showText("$" + NumberFormat.getInstance(Locale.US).format(Util.round(item.getUnitPrice())));
            stream.newLineAtOffset(65, 0);
            stream.showText("$" + NumberFormat.getInstance(Locale.US).format(Util.round(item.getExtPrice())));
            stream.endText();
            yCordinate = tempYcordinate;


Answer:

To right align the text you need to compute the width of the text to show and align the output position to

(right alignment position) - (text width)

Find below a small snippet which shows the principle. You need to amend the snippet for your needs.

import java.io.File;
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.PDType1Font;

public class RightAlignDemo {

    public static void main(String[] args) throws IOException {
        File file = new File("out.pdf");
        PDDocument doc = new PDDocument();
        PDPage page = new PDPage();
        doc.addPage(page);
        PDPageContentStream stream = new PDPageContentStream(doc, page);

        PDType1Font font = PDType1Font.TIMES_ROMAN;
        int fontSize = 12;

        stream.setFont(font, fontSize);

        double[] values = {0, 0.1, 0.01, 12.12, 123.12, 1234.12, 123456.12};

        int columnOneLeftX = 50;
        int columnTwoRightX = 170;
        int columnThreeOffsetX = 10;

        for (int i = 0; i < values.length; i++) {
            stream.beginText();
            stream.newLineAtOffset(columnOneLeftX, 700 - (i*10));
            // show some left aligned non fixed width text
            stream.showText("value " + values[i]);

            // format the double value with thousands separator and 
            // two decimals
            String text = String.format("%,.2f", values[i]);
            // get the width of the formated value
            float textWidth = getTextWidth(font, fontSize, text);
            // align the position to (right alignment minus text width)
            stream.newLineAtOffset(columnTwoRightX - textWidth, 0);
            stream.showText(text);

            // align the positon back to columnTwoRightX plus offset for
            // column three
            stream.newLineAtOffset(textWidth + columnThreeOffsetX, 0);
            stream.showText("description " + i);
            stream.endText();
        }

        stream.close();
        doc.save(file);
        doc.close();
    }

    private static float getTextWidth(PDType1Font font, int fontSize, 
            String text) throws IOException {
        return (font.getStringWidth(text) / 1000.0f) * fontSize;
    }
}

PDF output

Question:

I'm trying to create a PDF and then download it automatically. I'm using PDFBox to create the PDF and it saves locally just fine but as soon as I return it via ResponseEntity or byte[] it becomes blank. I want to use a post because I want to send a body of parameters that I need for the PDF.

Here's my controller

    @PostMapping(value="/documents/generate")
    ResponseEntity<?> generateSampleTag(@RequestBody SampleTag sampleTag) {
        log.info("inside generatePdfFromHtml method in DocumentController");
        try(ByteArrayOutputStream byteArrayOutputStream = freePdfService.generatePdf(sampleTag)) {
            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_PDF_VALUE);
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=sampleTag.pdf");
            headers.add("Expires", "0");
            headers.setCacheControl(CacheControl.noCache());
            headers.add("Pragma", "public");
            ByteArrayResource resource = new ByteArrayResource(byteArrayOutputStream.toByteArray());
            return new ResponseEntity<>(resource, headers, HttpStatus.OK);
        } catch (Exception e) {
            log.error(e.getMessage());
            return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
        }

    }

I've been trying a variety of input/output streams and header values and I'm just guessing at this point. Thanks for the help!

I answered below but my issue was Swagger couldn't download it correctly. Postman worked.


Answer:

I have not used PDFBox but it has worked for me before to send an inputStream as a response with a PDF mime type header.

Something like...

ByteArrayOutputStream byteArrayOutputStream = freePdfService.generatePdf(sampleTag)
headers.setContentType(MediaType.parseMediaType(MediaType.APPLICATION_PDF_VALUE));
return new ResponseEntity<>(new InputStreamResource(byteArrayOutputStream), headers, HttpStatus.OK);

Question:

I have a spring boot application which renders a XML document into PDF. The document contains French characters likeé à. While running the application through STS I have no issues the PDF is generated as expected. But while running the application through command line using java -jar target\application.jar the generated PDF has French characters as é Ã. I am converting the XML into byte[] and creating the PDF. I couldn't figure out a way out. Any help is much appreciated.


Answer:

Two options:

  1. Force the encoding with the file.encoding argument, such as -Dfile.encoding=utf-8.

    java -Dfile.encoding=utf-8 -jar target\application.jar
    
  2. (better) When you convert the xml file into a byte array, specify the encoding:

    Reader reader = new InputStreamReader(new FileInputStream("/path/to/xml/file"), StandardCharsets.UTF_8);
    // do your file reading ...
    

Question:

I can't find a way to stop warnings from PDFBox I am using in a psring boot application. For example:

2019-10-01 16:53:51.021  WARN 24564 --- [nio-8443-exec-2] o.a.pdfbox.pdmodel.font.PDType0Font      : No Unicode mapping for CID+4 (4) in font Calibri-Bold

2019-10-01 16:53:51.022  WARN 24564 --- [nio-8443-exec-2] o.a.pdfbox.pdmodel.font.PDCIDFontType2   : Failed to find a character mapping for 4 in Calibri-Bold

2019-10-01 16:53:51.022  WARN 24564 --- [nio-8443-exec-2] o.a.pdfbox.pdmodel.font.PDCIDFontType2   : Failed to find a character mapping for 4 in Calibri-Bold

I have tried: In Application file:

static {
  System.setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog");
  java.util.logging.Logger.getLogger("org.apache.pdfbox").setLevel(java.util.logging.Level.OFF);

  String[] loggers = {
    "org.apache.pdfbox.util.PDFStreamEngine",
    "org.apache.pdfbox.pdmodel.font.PDSimpleFont",
    "org.apache.pdfbox.pdmodel.font.PDFont",
    "org.apache.pdfbox.pdmodel.font.FontManager",
    "org.apache.pdfbox.pdfparser.PDFObjectStreamParser",
    "o.a.pdfbox.pdmodel.font.PDCIDFontType2",
    "org.apache.pdfbox.pdmodel.font.PDCIDFontType2",
    "o.a.pdfbox.pdmodel.font.PDType0Font",
    "org.apache.pdfbox.pdmodel.font.PDType0Font",
    "org.apache.pdfbox.pdmodel.font.PDType1Font"
  };
  for (String logger: loggers) {
    org.apache.log4j.Logger logpdfengine = org.apache.log4j.Logger
      .getLogger(logger);
    logpdfengine.setLevel(org.apache.log4j.Level.OFF);
  }

}

As parameter when running jar:

-Dorg.apache.commons.logging.Log=org.apache.commons.logging.impl.NoOpLog
-Dorg.slf4j.simpleLogger.defaultLogLevel=off

Within the PDFBox code, the log is set up using:

(import org.apache.commons.logging.LogFactory;)

private static final Log LOG = LogFactory.getLog(PDCIDFontType0.class);

LOG.warn("Found PFB but expected embedded CFF font " + fd.getFontName());

I've spent a long time trying a lot of things and trolled through the answers for similar questions in SO but not got anywhere.


Answer:

This is the configuration file I ended up using. I didn't include any logging releated dependencies or add any exclusions to pdfbox dependency, just added this file to the folder containing the application.properties file.

Filename is logback-spring.xml

The flooding logger was copied from how to change log levels of 3rd party library in java

<property name="LOGS" value="./logs" />

<appender name="Console"
    class="ch.qos.logback.core.ConsoleAppender">
    <layout class="ch.qos.logback.classic.PatternLayout">
        <Pattern>
            %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{1.}): %msg%n%throwable
        </Pattern>
    </layout>
</appender>

<appender name="RollingFile"
    class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOGS}/spring-boot-logger.log</file>
    <encoder
        class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <Pattern>%d %p %C{1.} [%t] %m%n</Pattern>
    </encoder>

    <rollingPolicy
        class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- rollover daily and when the file reaches 10 MegaBytes -->
        <fileNamePattern>${LOGS}/archived/spring-boot-logger-%d{yyyy-MM-dd}.%i.log
        </fileNamePattern>
        <timeBasedFileNamingAndTriggeringPolicy
            class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
            <maxFileSize>10MB</maxFileSize>
        </timeBasedFileNamingAndTriggeringPolicy>
    </rollingPolicy>
</appender>

<!-- LOG everything at INFO level -->
<root level="info">
    <appender-ref ref="RollingFile" />
    <appender-ref ref="Console" />
</root>

<!-- LOG "com.baeldung*" at TRACE level -->
<logger name="org.apache" level="ERROR" additivity="false">
    <appender-ref ref="RollingFile" />
    <appender-ref ref="Console" />
</logger>

<logger name="flooding logger" level="ERROR" additivity="false">
  <appender-ref ref="Console"/>