package com.door43.translationstudio.core;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.door43.translationstudio.AppContext;
import com.door43.translationstudio.R;
import com.door43.translationstudio.spannables.Span;
import com.door43.translationstudio.spannables.USFMVerseSpan;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.BaseFont;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfPageEventHelper;
import com.itextpdf.text.pdf.PdfTemplate;
import com.itextpdf.text.pdf.PdfWriter;
import com.itextpdf.text.pdf.draw.VerticalPositionMark;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Created by joel on 11/12/2015.
*/
public class PdfPrinter extends PdfPageEventHelper {
private static final float VERTICAL_PADDING = 72.0f; // 1 inch
private static final float HORIZONTAL_PADDING = 72.0f; // 1 inch
private final TargetTranslation targetTranslation;
private final Context context;
private final Font titleFont;
private final Font chapterFont;
private final Font bodyFont;
private final Font boldBodyFont;
private final Font underlineBodyFont;
private final Font subFont;
private final Font headingFont;
private final TranslationFormat format;
private final Library library;
private final SourceTranslation sourceTranslation;
private final Font superScriptFont;
private final BaseFont baseFont;
private final File imagesDir;
private boolean includeMedia = true;
private boolean includeIncomplete = true;
private final Map<String, PdfTemplate> tocPlaceholder = new HashMap<>();
private final Map<String, Integer> pageByTitle = new HashMap<>();
private final float PAGE_NUMBER_FONT_SIZE = 10;
private PdfWriter writer;
private Paragraph mCurrentParagraph;
public PdfPrinter(Context context, Library library, TargetTranslation targetTranslation, TranslationFormat format, String fontPath, File imagesDir) throws IOException, DocumentException {
this.targetTranslation = targetTranslation;
this.context = context;
this.format = format;
this.library = library;
this.imagesDir = imagesDir;
this.sourceTranslation = library.getDefaultSourceTranslation(targetTranslation.getProjectId(), "en");
baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
titleFont = new Font(baseFont, 25, Font.BOLD);
chapterFont = new Font(baseFont, 20);
bodyFont = new Font(baseFont, 10);
boldBodyFont = new Font(baseFont, 10, Font.BOLD);
headingFont = new Font(baseFont, 14, Font.BOLD);
underlineBodyFont = new Font(baseFont, 10, Font.UNDERLINE);
subFont = new Font(baseFont, 10, Font.ITALIC);
superScriptFont = new Font(baseFont, 9);
superScriptFont.setColor(94, 94, 94);
}
/**
* Include media (images) in the pdf
* @param include
*/
public void includeMedia(boolean include) {
this.includeMedia = include;
}
/**
* Include incomplete translations
* @param include
*/
public void includeIncomplete(boolean include) {
this.includeIncomplete = include;
}
public File print() throws Exception {
File tempFile = File.createTempFile(targetTranslation.getId(), "pdf");
Document document = new Document(PageSize.LETTER, HORIZONTAL_PADDING, HORIZONTAL_PADDING, VERTICAL_PADDING, VERTICAL_PADDING);
writer = PdfWriter.getInstance(document, new FileOutputStream(tempFile));
writer.setPageEvent(this);
document.open();
addMetaData(document);
addTitlePage(document);
addLicensePage(document);
addTOC(document);
addContent(document);
document.close();
return tempFile;
}
private void addTOC(Document document) throws DocumentException {
String toc = AppContext.context().getResources().getString(R.string.table_of_contents);
com.itextpdf.text.Chapter intro = new com.itextpdf.text.Chapter(new Paragraph(toc, chapterFont), 0);
intro.setNumberDepth(0);
document.add(intro);
for(ChapterTranslation c:targetTranslation.getChapterTranslations()) {
if(!includeIncomplete && !c.isTitleFinished() && !library.getChapter(sourceTranslation, c.getId()).title.isEmpty()) {
continue;
}
// write chapter title
final String title = chapterTitle(c);
Chunk chunk = new Chunk(title).setLocalGoto(title);
document.add(new Paragraph(chunk));
// add placeholder for page reference
document.add(new VerticalPositionMark() {
@Override
public void draw(final PdfContentByte canvas, final float llx, final float lly, final float urx, final float ury, final float y) {
final PdfTemplate createTemplate = canvas.createTemplate(50, 50);
tocPlaceholder.put(title, createTemplate);
canvas.addTemplate(createTemplate, urx - 50, y);
}
});
}
document.newPage();
}
/**
* Adds file meta data
* @param document
*/
private void addMetaData(Document document) {
ProjectTranslation projectTranslation = targetTranslation.getProjectTranslation();
document.addTitle(projectTranslation.getTitle());
document.addSubject(projectTranslation.getDescription());
for(NativeSpeaker ns:targetTranslation.getContributors()) {
document.addAuthor(ns.name);
document.addCreator(ns.name);
}
document.addCreationDate();
document.addLanguage(targetTranslation.getTargetLanguageName());
document.addKeywords("format=" + format.getName());
}
/**
* Adds the title page
* @param document
* @throws DocumentException
*/
private void addTitlePage(Document document) throws DocumentException {
Paragraph preface = new Paragraph();
preface.setAlignment(Element.ALIGN_CENTER);
addEmptyLine(preface, 1);
// book title
ProjectTranslation projectTranslation = targetTranslation.getProjectTranslation();
Paragraph titleParagraph = (new Paragraph(projectTranslation.getTitle(), titleFont));
titleParagraph.setAlignment(Element.ALIGN_CENTER);
preface.add(titleParagraph);
addEmptyLine(preface, 1);
// book description
preface.add(new Paragraph(projectTranslation.getDescription(), subFont));
// table for vertical alignment
PdfPTable table = new PdfPTable(1);
table.setWidthPercentage(100);
PdfPCell cell = new PdfPCell();
cell.setBorder(Rectangle.NO_BORDER);
cell.setMinimumHeight(document.getPageSize().getHeight() - VERTICAL_PADDING * 2);
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
cell.addElement(preface);
table.addCell(cell);
document.add(table);
document.newPage();
}
private String chapterTitle(ChapterTranslation c) {
String title;
if(c.title.isEmpty()) {
title = String.format(context.getResources().getString(R.string.label_chapter_title_detailed), "" + Integer.parseInt(c.getId()));
} else {
title = c.title;
}
return title;
}
private void addChapterPage(Document document, ChapterTranslation c) throws DocumentException {
// title
String title = chapterTitle(c);
Anchor anchor = new Anchor(title, chapterFont);
anchor.setName(c.title);
Paragraph chapterParagraph = new Paragraph(anchor);
chapterParagraph.setAlignment(Element.ALIGN_CENTER);
com.itextpdf.text.Chapter chapter = new com.itextpdf.text.Chapter(chapterParagraph, Integer.parseInt(c.getId()));
chapter.setNumberDepth(0);
// table for vertical alignment
PdfPTable table = new PdfPTable(1);
table.setWidthPercentage(100);
PdfPCell cell = new PdfPCell();
cell.setBorder(Rectangle.NO_BORDER);
cell.setMinimumHeight(document.getPageSize().getHeight() - VERTICAL_PADDING * 2);
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
// cell.addElement(chapter);
table.addCell(cell);
// place chapter title on it's own page
document.newPage();
// document.add(table);
document.add(chapter);
// update TOC
PdfTemplate template = tocPlaceholder.get(title);
template.beginText();
template.setFontAndSize(baseFont, PAGE_NUMBER_FONT_SIZE);
template.setTextMatrix(50 - baseFont.getWidthPoint(String.valueOf(writer.getPageNumber()), PAGE_NUMBER_FONT_SIZE), 0);
template.showText(String.valueOf(writer.getPageNumber()));
template.endText();
document.newPage();
}
/**
* Adds the content of the book
* @param document
*/
private void addContent(Document document) throws DocumentException, IOException {
for(ChapterTranslation c:targetTranslation.getChapterTranslations()) {
if(includeIncomplete || c.isTitleFinished() || library.getChapter(sourceTranslation, c.getId()).title.isEmpty()) {
addChapterPage(document, c);
}
// chapter body
for(FrameTranslation f:targetTranslation.getFrameTranslations(c.getId(), this.format)) {
if(includeIncomplete || f.isFinished()) {
if(includeMedia && this.format == TranslationFormat.DEFAULT) {
// TODO: 11/13/2015 insert frame images if we have them.
// TODO: 11/13/2015 eventually we need to provide the directory where to find these images which will be downloaded not in assets
try {
File imageFile = new File(imagesDir, "360px/" + targetTranslation.getProjectId() + "-" + f.getComplexId() + ".jpg");
if(imageFile.exists()) {
addImage(document, imageFile.getAbsolutePath());
}
} catch (Exception e) {
e.printStackTrace();
}
}
// TODO: 11/13/2015 render body according to the format
Paragraph paragraph = new Paragraph("", bodyFont);
String body = f.body;
if(format == TranslationFormat.USFM) {
addUSFM(paragraph, f.body);
} else {
paragraph.add(body);
}
document.add(paragraph);
document.add(new Paragraph(" "));
}
}
// chapter reference
if(includeIncomplete || c.isReferenceFinished()) {
document.add(new Paragraph(c.reference, subFont));
}
}
}
private void addUSFM(Paragraph paragraph, String usfm) {
Pattern pattern = Pattern.compile(USFMVerseSpan.PATTERN);
Matcher matcher = pattern.matcher(usfm);
int lastIndex = 0;
while(matcher.find()) {
// add preceeding text
paragraph.add(usfm.substring(lastIndex, matcher.start()));
// add verse
Span verse = new USFMVerseSpan(matcher.group(1));
Chunk chunk = new Chunk();
chunk.setFont(superScriptFont);
chunk.setTextRise(5f);
if (verse != null) {
chunk.append(verse.getHumanReadable().toString());
} else {
// failed to parse the verse
chunk.append(usfm.subSequence(lastIndex, matcher.end()).toString());
}
chunk.append(" ");
paragraph.add(chunk);
lastIndex = matcher.end();
}
paragraph.add(usfm.subSequence(lastIndex, usfm.length()).toString());
}
private static void addEmptyLine(Paragraph paragraph, int number) {
for (int i = 0; i < number; i++) {
paragraph.add(new Paragraph(" "));
}
}
/**
* Add image from a file path
*
* @param document
* @param path
* @throws DocumentException
* @throws IOException
*/
public static void addImage(Document document, String path) throws DocumentException, IOException {
Image image = Image.getInstance(path);
image.setAlignment(Element.ALIGN_CENTER);
if(image.getScaledWidth() > pageWidth(document) || image.getScaledHeight() > pageHeight(document)) {
image.scaleToFit(pageWidth(document), pageHeight(document));
}
document.add(new Chunk(image, 0, 0, true));
}
/**
* Add Image from an input stream
* @param document
* @param is
* @throws DocumentException
* @throws IOException
*/
public static void addImage(Document document, InputStream is) throws DocumentException, IOException {
Bitmap bmp = BitmapFactory.decodeStream(is);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bmp.compress(Bitmap.CompressFormat.PNG, 100, stream);
Image image = Image.getInstance(stream.toByteArray());
image.setAlignment(Element.ALIGN_CENTER);
if(image.getScaledWidth() > pageWidth(document) || image.getScaledHeight() > pageHeight(document)) {
image.scaleToFit(pageWidth(document), pageHeight(document));
}
document.add(new Chunk(image, 0, 0, true));
}
/**
* Returns the height of the printable area of the page
* @param document
* @return
*/
private static float pageHeight(Document document) {
return document.getPageSize().getHeight() - VERTICAL_PADDING * 2;
}
/**
* Returns the width of the printable area of the page
* @param document
* @return
*/
private static float pageWidth(Document document) {
return document.getPageSize().getWidth() - HORIZONTAL_PADDING * 2;
}
@Override
public void onChapter(final PdfWriter writer, final Document document, final float paragraphPosition, final Paragraph title) {
this.pageByTitle.put(title.getContent(), writer.getPageNumber());
}
@Override
public void onSection(final PdfWriter writer, final Document document, final float paragraphPosition, final int depth, final Paragraph title) {
this.pageByTitle.put(title.getContent(), writer.getPageNumber());
}
@Override
public void onEndPage(PdfWriter writer, Document document) {
PdfContentByte cb = writer.getDirectContent();
cb.saveState();
String text = "" + writer.getPageNumber();
// place page number just within the margin
float textBase = document.bottom() - PAGE_NUMBER_FONT_SIZE;
cb.beginText();
cb.setFontAndSize(baseFont, PAGE_NUMBER_FONT_SIZE);
cb.setTextMatrix((document.right() / 2) + HORIZONTAL_PADDING / 2, textBase);
cb.showText(text);
cb.endText();
cb.restoreState();
}
/**
* add the license from resource
* @param document
* @throws DocumentException
*/
private void addLicensePage(Document document) throws DocumentException {
// title
String title = "";
Anchor anchor = new Anchor(title, chapterFont);
anchor.setName("name");
Paragraph chapterParagraph = new Paragraph(anchor);
chapterParagraph.setAlignment(Element.ALIGN_CENTER);
com.itextpdf.text.Chapter chapter = new com.itextpdf.text.Chapter(chapterParagraph, 0);
chapter.setNumberDepth(0);
// table for vertical alignment
PdfPTable table = new PdfPTable(1);
table.setWidthPercentage(100);
PdfPCell cell = new PdfPCell();
cell.setBorder(Rectangle.NO_BORDER);
cell.setMinimumHeight(document.getPageSize().getHeight() - VERTICAL_PADDING * 2);
cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
// cell.addElement(chapter);
table.addCell(cell);
// place chapter title on it's own page
document.newPage();
document.add(chapter);
// translate simple html to paragraphs
String license = AppContext.context().getResources().getString(R.string.license_pdf);
license = license.replace("•", "\u2022");
// license = license.replace("<p>", "<br/>");
// license = license.replace("</p>", "<br/>");
// license = license.replace("<h2>", "<br/>");
// license = license.replace("</h2>", "<br/>");
// String[] lines = license.split("<br/>");
// for (String line : lines) {
// Paragraph paragraph = new Paragraph(line, bodyFont);
// document.add(paragraph);
// }
mCurrentParagraph = null;
parseHtml( document, license, 0);
nextParagraph(document);
document.newPage();
}
/**
* convert basic html to pdf chunks and add to document
* @param document
* @param text
* @param pos
* @throws DocumentException
*/
private void parseHtml(Document document, String text, int pos) throws DocumentException {
if(text == null) {
return;
}
int length = text.length();
FoundHtml foundHtml;
while(pos < length) {
foundHtml = getNextHtml(text, pos);
if(null == foundHtml) {
break;
}
if(foundHtml.startPos > pos) {
String beforeText = text.substring(pos, foundHtml.startPos);
addHtmlChunk(beforeText, bodyFont);
}
if("b".equals(foundHtml.html)) { // bold
addHtmlChunk(foundHtml.enclosed, boldBodyFont);
} else if((foundHtml.html.length() > 0) && (foundHtml.html.charAt(0) == 'h')) { // header
nextParagraph(document);
mCurrentParagraph = new Paragraph(foundHtml.enclosed, headingFont);
nextParagraph(document);
} else if((foundHtml.html.length() > 0) && (foundHtml.html.charAt(0) == 'a')) { // anchor
addHtmlChunk(foundHtml.enclosed, underlineBodyFont);
} else if("br".equals(foundHtml.html)) { // line break
nextParagraph(document);
} else if("p".equals(foundHtml.html)) { // line break
blankLine(document);
parseHtml( document, foundHtml.enclosed, 0);
nextParagraph(document);
} else { // anything else just strip off the html tag
parseHtml( document, foundHtml.enclosed, 0);
}
pos = foundHtml.htmlFinishPos;
}
if(pos < length) {
String rest = text.substring(pos);
addHtmlChunk(rest, bodyFont);
}
}
/**
* add text to current paragraph, trim white space
* @param text
* @param font
* @throws DocumentException
*/
private void addHtmlChunk(String text, Font font) throws DocumentException {
if((text != null) && !text.isEmpty()) {
Character c = text.charAt(0);
text = text.replace("\n","");
while((text.length() > 1) && (" ".equals(text.substring(0,2)))) { // remove extra leading space
text = text.substring(1);
}
while((text.length() > 1) && (" ".equals(text.substring(text.length() - 2)))) { // remove extra leading space
text = text.substring(0, text.length() - 1);
}
Chunk chunk = new Chunk(text, font);
addChunkToParagraph(chunk);
}
}
/**
* start a new paragraph
* @param document
* @throws DocumentException
*/
private void nextParagraph(Document document) throws DocumentException {
if(mCurrentParagraph != null) {
document.add(mCurrentParagraph);
}
mCurrentParagraph = null;
}
/**
* insert a blank paragraph
* @param document
* @throws DocumentException
*/
private void blankLine(Document document) throws DocumentException {
nextParagraph(document);
mCurrentParagraph = new Paragraph(" ", bodyFont);
nextParagraph(document);
}
/**
* add a chunk to current paragraph
* @param chunk
* @throws DocumentException
*/
private void addChunkToParagraph(Chunk chunk) throws DocumentException {
if(null == mCurrentParagraph) {
mCurrentParagraph = new Paragraph("", bodyFont);
}
mCurrentParagraph.add(chunk);
}
/**
* get next html tag from start pos
* @param text
* @param startPos
* @return
*/
private FoundHtml getNextHtml(String text, int startPos) {
int pos = text.indexOf("<", startPos);
if(pos < 0) {
return null;
}
int end = text.indexOf(">", pos + 1);
int length = text.length();
if(end < 0) {
return new FoundHtml(text.substring(pos + 1), pos, length, "");
}
String token = text.substring(pos + 1, end);
if(!token.isEmpty() && (token.charAt(token.length() - 1) == '/')) {
return new FoundHtml(token.substring(0, token.length() - 1), pos, end + 1, "");
}
String[] parts = token.split(" "); // ignore attibutes
String endToken = "</" + parts[0] + ">";
int finish = text.indexOf(endToken, end + 1);
if(finish < 0) { // if end token not found, then stop at next
int next = text.indexOf("<", end + 1);
if(next < 0) {
return new FoundHtml(token, pos, length, text.substring(end + 1, length));
} else {
return new FoundHtml(token, pos, next, text.substring(end + 1, next));
}
}
int htmlFinishPos = finish + endToken.length();
return new FoundHtml(token, pos, htmlFinishPos, text.substring(end + 1, finish));
}
/**
* class for keeping track of an html tag that was found, it's name, it's contents, and position
*/
private class FoundHtml {
public String html;
public String enclosed;
public int startPos;
public int htmlFinishPos;
public FoundHtml(String html, int startPos, int htmlFinishPos, String enclosed) {
this.html = html;
this.startPos = startPos;
this.htmlFinishPos = htmlFinishPos;
this.enclosed = enclosed;
}
}
}