package net.rubygrapefruit.docs.pdf;
import com.itextpdf.text.*;
import com.itextpdf.text.Document;
import com.itextpdf.text.pdf.PdfWriter;
import net.rubygrapefruit.docs.model.*;
import net.rubygrapefruit.docs.model.Error;
import net.rubygrapefruit.docs.model.List;
import net.rubygrapefruit.docs.model.ListItem;
import net.rubygrapefruit.docs.model.Paragraph;
import net.rubygrapefruit.docs.model.Section;
import net.rubygrapefruit.docs.renderer.BuildableChunk;
import net.rubygrapefruit.docs.renderer.RenderableDocument;
import net.rubygrapefruit.docs.renderer.SingleFileRenderer;
import net.rubygrapefruit.docs.renderer.TitleBlock;
import net.rubygrapefruit.docs.theme.TextTheme;
import net.rubygrapefruit.docs.theme.Theme;
import java.io.OutputStream;
import java.math.BigDecimal;
public class PdfRenderer extends SingleFileRenderer {
private BigDecimal lineHeight;
@Override
protected void doRender(RenderableDocument document, Theme theme, OutputStream stream) throws Exception {
TextTheme textTheme = theme.getAspect(TextTheme.class);
lineHeight = BigDecimal.valueOf(14, 1);
if (textTheme != null) {
lineHeight = textTheme.getLineSpacing();
}
FontStack fonts = new FontStack(textTheme);
// TODO - theme margins
com.itextpdf.text.Document pdfDocument = new com.itextpdf.text.Document(PageSize.A4, 40, 20, 40, 20);
PdfWriter.getInstance(pdfDocument, stream);
pdfDocument.open();
writeDocument(document, fonts, pdfDocument);
pdfDocument.close();
}
private void writeDocument(RenderableDocument document, FontStack fonts, com.itextpdf.text.Document pdfDocument)
throws DocumentException {
boolean first = true;
for (BuildableChunk chunk : document.getContents()) {
if (!first) {
pdfDocument.newPage();
}
first = false;
for (Block block : chunk.getContents()) {
if (block instanceof Component) {
writeComponent((Component) block, fonts, 1, pdfDocument);
} else if (block instanceof TitleBlock) {
writeTitleBlock((TitleBlock) block, fonts, pdfDocument);
} else if (block instanceof Error) {
pdfDocument.add(createErrorBlock((Error) block, fonts));
} else {
writeComponentBlock(block, fonts, pdfDocument);
}
}
}
}
private void writeTitleBlock(TitleBlock titleBlock, FontStack fonts, Document pdfDocument) throws DocumentException {
writeTitle(titleBlock.getComponent(), fonts, 0, pdfDocument);
}
private void writeComponent(Component component, FontStack fonts, int depth, com.itextpdf.text.Document target)
throws DocumentException {
writeTitle(component, fonts, 0, target);
for (Block block : component.getContents()) {
if (block instanceof Section) {
Section child = (Section) block;
writeSection(child, fonts, depth == 0 ? 0 : 1, target);
} else if (block instanceof Component) {
Component child = (Component) block;
writeComponent(child, fonts, depth + 1, target);
} else {
writeComponentBlock(block, fonts, target);
}
}
}
private void writeSection(Section section, FontStack fonts, int depth, com.itextpdf.text.Document target)
throws DocumentException {
writeTitle(section, fonts, depth, target);
for (Block block : section.getContents()) {
if (block instanceof Section) {
Section child = (Section) block;
writeSection(child, fonts, depth + 1, target);
} else {
writeComponentBlock(block, fonts, target);
}
}
}
private void writeTitle(Component component, FontStack fonts, int depth, com.itextpdf.text.Document target)
throws DocumentException {
if (component.getTitle().isEmpty()) {
return;
}
com.itextpdf.text.Paragraph title = new com.itextpdf.text.Paragraph();
FontStack headerFonts = fonts.getHeader(depth);
title.setFont(headerFonts.getBase());
Anchor anchor = new Anchor();
anchor.setFont(title.getFont());
anchor.setName(component.getId());
title.add(anchor);
writeContents(component.getTitle(), headerFonts, new AnchorBackedContainer(title, anchor));
// TODO - theme spacing
title.setSpacingBefore(15);
title.setSpacingAfter(8);
target.add(title);
}
private void writeComponentBlock(Block block, FontStack fonts, Document target) throws DocumentException {
convertBlock(block, fonts, new DocumentBackedElementContainer(target));
}
private void convertBlock(Block block, FontStack fonts, ElementContainer container) throws DocumentException {
if (block instanceof Paragraph) {
Paragraph paragraph = (Paragraph) block;
com.itextpdf.text.Paragraph pdfParagraph = new com.itextpdf.text.Paragraph();
pdfParagraph.setFont(fonts.getBase());
pdfParagraph.setAlignment(Element.ALIGN_JUSTIFIED);
// TODO - theme spacing
pdfParagraph.setSpacingBefore(4);
pdfParagraph.setSpacingAfter(4);
pdfParagraph.setMultipliedLeading(lineHeight.floatValue());
writeContents(paragraph, fonts, new PhraseBackedContainer(pdfParagraph));
container.add(pdfParagraph);
} else if (block instanceof ItemisedList) {
ItemisedList list = (ItemisedList) block;
// TODO - theme indent
com.itextpdf.text.List pdfList = new com.itextpdf.text.List(false, 24);
pdfList.setListSymbol("\u2022 ");
addItems(list, fonts, pdfList);
container.add(pdfList);
} else if (block instanceof OrderedList) {
OrderedList list = (OrderedList) block;
// TODO - theme indent
com.itextpdf.text.List pdfList = new com.itextpdf.text.List(true, 24);
addItems(list, fonts, pdfList);
container.add(pdfList);
} else if (block instanceof ProgramListing) {
ProgramListing programListing = (ProgramListing) block;
com.itextpdf.text.Paragraph para = new com.itextpdf.text.Paragraph();
para.setFont(fonts.getMonospaced().getBase());
para.add(programListing.getText());
container.add(para);
} else if (block instanceof Example) {
Example example = (Example) block;
for (Block childBlock : example.getContents()) {
convertBlock(childBlock, fonts, container);
}
if (!example.getTitle().isEmpty()) {
com.itextpdf.text.Paragraph title = new com.itextpdf.text.Paragraph();
writeContents(example.getTitle(), fonts.getBold(), new PhraseBackedContainer(title));
container.add(title);
}
} else if (block instanceof Error) {
container.add(createErrorBlock((Error) block, fonts));
} else {
throw new IllegalStateException(String.format("Don't know how to render block of type '%s'.",
block.getClass().getSimpleName()));
}
}
private Element createErrorBlock(Error error, FontStack fonts) {
com.itextpdf.text.Paragraph paragraph = new com.itextpdf.text.Paragraph();
paragraph.setFont(fonts.getError());
paragraph.add(error.getMessage());
return paragraph;
}
private void writeContents(InlineContainer inlineContainer, FontStack fonts, PhraseContainer owner) {
for (Inline inline : inlineContainer.getContents()) {
if (inline instanceof Text) {
Text text = (Text) inline;
owner.add(new Chunk(text.getText(), fonts.getBase()));
} else if (inline instanceof Code || inline instanceof Literal || inline instanceof ClassName) {
FontStack monospacedFonts = fonts.getMonospaced();
writeContents((InlineContainer) inline, monospacedFonts, owner);
} else if (inline instanceof Emphasis) {
FontStack italicFonts = fonts.getItalic();
writeContents((InlineContainer) inline, italicFonts, owner);
} else if (inline instanceof CrossReference) {
writeCrossReference((CrossReference) inline, fonts, owner);
} else if (inline instanceof Link) {
writeLink((Link) inline, fonts, owner);
} else if (inline instanceof Error) {
Error error = (Error) inline;
owner.add(new Chunk(error.getMessage(), fonts.getError()));
} else {
throw new IllegalStateException(String.format("Don't know how to render inline of type '%s'.",
inline.getClass().getSimpleName()));
}
}
}
private void writeCrossReference(CrossReference crossReference, FontStack fonts, PhraseContainer owner) {
Referenceable target = crossReference.getTarget();
Anchor anchor = new Anchor();
FontStack linkFonts = fonts.getUnderline();
anchor.setFont(linkFonts.getBase());
writeContents(crossReference, fonts, new PhraseBackedContainer(anchor));
anchor.setReference("#" + target.getId());
owner.add(anchor);
}
private void writeLink(Link link, FontStack fonts, PhraseContainer owner) {
Anchor anchor = new Anchor();
FontStack linkFonts = fonts.getUnderline();
anchor.setFont(linkFonts.getBase());
writeContents(link, fonts, new PhraseBackedContainer(anchor));
anchor.setReference(link.getTarget().toString());
owner.add(anchor);
}
private void addItems(List list, FontStack fonts, com.itextpdf.text.List pdfList) throws DocumentException {
for (ListItem item : list.getItems()) {
com.itextpdf.text.ListItem pdfItem = new com.itextpdf.text.ListItem();
pdfList.add(pdfItem);
// TODO - theme spacing
pdfItem.setSpacingBefore(4);
pdfItem.setSpacingAfter(4);
ListItemBackedElementContainer container = new ListItemBackedElementContainer(pdfItem);
for (Block childBlock : item.getContents()) {
convertBlock(childBlock, fonts, container);
}
pdfItem.getListSymbol().setFont(fonts.getBase());
}
}
private interface ElementContainer {
void add(Element element) throws DocumentException;
}
private static class DocumentBackedElementContainer implements ElementContainer {
private final Document document;
private DocumentBackedElementContainer(Document document) {
this.document = document;
}
public void add(Element element) throws DocumentException {
document.add(element);
}
}
private static class ListItemBackedElementContainer implements ElementContainer {
private final com.itextpdf.text.ListItem listItem;
private ListItemBackedElementContainer(com.itextpdf.text.ListItem listItem) {
this.listItem = listItem;
}
public void add(Element element) throws DocumentException {
listItem.add(element);
}
}
private interface PhraseContainer {
void add(Chunk chunk);
void add(Phrase phrase);
}
private static class PhraseBackedContainer implements PhraseContainer {
private final Phrase phrase;
private PhraseBackedContainer(Phrase phrase) {
this.phrase = phrase;
}
public void add(Chunk chunk) {
phrase.add(chunk);
}
public void add(Phrase phrase) {
this.phrase.add(phrase);
}
}
private static class AnchorBackedContainer implements PhraseContainer {
private final Phrase parent;
private Phrase current;
private AnchorBackedContainer(Phrase parent, Anchor anchor) {
this.parent = parent;
current = anchor;
}
public void add(Chunk chunk) {
current.add(chunk);
}
public void add(Phrase phrase) {
if (phrase instanceof Anchor) {
Anchor nestedAnchor = (Anchor) phrase;
parent.add(nestedAnchor);
current = parent;
} else {
current.add(phrase);
}
}
}
}