package co.smartreceipts.android.workers.reports.pdf.pdfbox;
import android.support.annotation.NonNull;
import com.google.common.base.Preconditions;
import com.tom_roush.pdfbox.pdmodel.PDDocument;
import com.tom_roush.pdfbox.pdmodel.PDPage;
import com.tom_roush.pdfbox.pdmodel.PDPageContentStream;
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle;
import com.tom_roush.pdfbox.pdmodel.graphics.image.PDImageXObject;
import com.tom_roush.pdfbox.util.awt.AWTColor;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import co.smartreceipts.android.workers.reports.pdf.colors.PdfColorStyle;
import co.smartreceipts.android.workers.reports.pdf.fonts.PdfFontSpec;
import co.smartreceipts.android.workers.reports.pdf.utils.HeavyHandedReplaceIllegalCharacters;
/**
* This writer class directly interacts with the underlying PdfBox layer in order to abstract that
* away and provide simple writing functionality. Please note that by default, PdfBox treats the
* coordinate (0, 0) as the bottom-left of the page. Since I find it more meaningful to treat (0, 0)
* as the top-left of the page (ie the same as Android), all public methods in this class will
* also implicitly handle this translation.
*/
public class PdfBoxWriter {
private final PDDocument mDocument;
private final PdfBoxContext mContext;
private final PdfBoxPageDecorations mPageDecorations;
private final List<PDPage> mPages;
private float currentYPosition;
private float topOfPageYPosition = -1;
private float leftOfPageXPosition = 0;
private PDPageContentStream contentStream;
public PdfBoxWriter(@NonNull PDDocument doc,
@NonNull PdfBoxContext context,
@NonNull PdfBoxPageDecorations pageDecorations) throws IOException {
mDocument = doc;
mContext = context;
mPageDecorations = pageDecorations;
mPages = new ArrayList<>();
}
/**
* Creates a new PDF page with the decorated header and footer
*
* @throws IOException if this operation fails
*/
public void newPage() throws IOException {
if (contentStream != null) {
contentStream.close();
}
PDPage page = new PDPage(mContext.getPageSize());
mPages.add(page);
contentStream = new PDPageContentStream(mDocument, page);
mPageDecorations.writeHeader(contentStream);
currentYPosition = page.getMediaBox().getHeight() - mContext.getPageMarginVertical() - mPageDecorations.getHeaderHeight();
leftOfPageXPosition = mContext.getPageMarginHorizontal();
topOfPageYPosition = currentYPosition;
mPageDecorations.writeFooter(contentStream);
}
/**
* Prints a Rectangle in a particular color
*
* @param color the {@link AWTColor} to draw
* @param x the x-coordinate location, where 0 is the left of the page
* @param y the y-coordinate location, where 0 is the top of the page
* @param width the width that this image requires
* @param height the height that this image requires
*
* @throws IOException if something fails during the printing process
*/
public void printRectangle(@NonNull AWTColor color, float x, float y, float width, float height) throws IOException {
final PDRectangle rect = new PDRectangle(leftOfPageXPosition + x, swapYCoordinate(y) - height, width, height);
contentStream.setNonStrokingColor(color);
contentStream.addRect(rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getWidth(), rect.getHeight());
contentStream.fill();
// Reset to default afterwards
contentStream.setNonStrokingColor(mContext.getColorManager().getColor(PdfColorStyle.Default));
}
/**
* Prints a {@link PDImageXObject} (usually an image or another PDF) inside our PDF
*
* @param image the {@link PDImageXObject} to print
* @param x the x-coordinate location, where 0 is the left of the page
* @param y the y-coordinate location, where 0 is the top of the page
* @param width the width that this image requires
* @param height the height that this image requires
*
* @throws IOException if something fails during the printing process
*/
public void printPDImageXObject(@NonNull PDImageXObject image, float x, float y, float width, float height) throws IOException {
contentStream.drawImage(image, leftOfPageXPosition + x, swapYCoordinate(y) - height, width, height);
}
/**
* Prints a given string in the desired font/color combination at the specified x/y coordinates
*
* @param string the {@link String} to print
* @param font the desired {@link PdfFontSpec} to write the text in
* @param color the preferred {@link AWTColor} to use
* @param x the x-coordinate location, where 0 is the left of the page
* @param y the y-coordinate location, where 0 is the top of the page
*
* @throws IOException if something fails during the printing process
*/
public void printText(@NonNull String string, @NonNull PdfFontSpec font, @NonNull AWTColor color, float x, float y) throws IOException {
contentStream.setFont(font.getFont(), font.getSize());
contentStream.setNonStrokingColor(color);
contentStream.beginText();
contentStream.newLineAtOffset(leftOfPageXPosition + x, swapYCoordinate(y));
contentStream.showText(HeavyHandedReplaceIllegalCharacters.getSafeString(string));
contentStream.endText();
}
/**
* Writes the file and closes our stream
*
* @throws IOException if we fail to write this item
*/
public void writeAndClose() throws IOException {
if (contentStream != null) {
contentStream.close();
}
for (PDPage page : mPages) {
mDocument.addPage(page);
}
}
/**
* Please note that by default, PdfBox treats the coordinate (0, 0) as the bottom-left of the
* page. Since I find it more meaningful to treat (0, 0) as the top-left of the page (ie the
* same as Android), we can use this method to perform the internal swap that PDF box requires.
*
* @param y the y-coordinate location, where 0 is the TOP of the page
* @return y the y-coordinate location, where 0 is the BOTTOM of the page
*/
private float swapYCoordinate(float y) {
Preconditions.checkArgument(topOfPageYPosition > 0, "You cannot write any values until creating a new page");
return topOfPageYPosition - y;
}
}