be.roots.taconic.pricingguide.service.PDFServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for be.roots.taconic.pricingguide.service.PDFServiceImpl.java

Source

package be.roots.taconic.pricingguide.service;

/**
 *  This file is part of the Taconic Pricing Guide generator.  This code will
 *  generate a full featured PDF Pricing Guide by using using iText
 *  (http://www.itextpdf.com) based on JSON files.
 *
 *  Copyright (C) 2015  Roots nv
 *  Authors: Koen Dehaen (koen.dehaen@roots.be)
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  For more information, please contact Roots nv at this address: support@roots.be
 *
 */

import be.roots.taconic.pricingguide.domain.*;
import be.roots.taconic.pricingguide.respository.TemplateRepository;
import be.roots.taconic.pricingguide.util.*;
import com.itextpdf.text.*;
import com.itextpdf.text.html.simpleparser.HTMLWorker;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.draw.DottedLineSeparator;
import com.itextpdf.text.pdf.draw.LineSeparator;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;

@Service
public class PDFServiceImpl implements PDFService {

    private final static Logger LOGGER = Logger.getLogger(PDFServiceImpl.class);
    private final static String DELIMETER = "-!-!-!-!-!-!-!-";

    @Value("${link.email}")
    private String emailLink;

    @Value("${link.website}")
    private String websiteLink;

    @Value("${link.contactUs}")
    private String contactUsUrl;

    @Value("${document.title}")
    private String documentTitle;

    @Value("${cover.title.1}")
    private String coverTitle1;

    @Value("${cover.title.2}")
    private String coverTitle2;

    @Value("${cover.title.3}")
    private String coverTitle3;

    @Value("${cover.title.4}")
    private String coverTitle4;

    @Value("${disclaimer}")
    private String disclaimer;

    @Autowired
    private DefaultService defaultService;

    @Autowired
    private TemplateRepository templateRepository;

    @Autowired
    private GitService gitService;

    private PDFTemplates pdfTemplate;

    @PostConstruct
    private void init() throws IOException {

        final String urlAsString = defaultService.getBaseUrl() + "/pricing_guide_files/pdf_list.json";
        final String pdfTemplateAsJson = HttpUtil.readString(urlAsString, defaultService.getUserName(),
                defaultService.getPassword());
        if (pdfTemplateAsJson == null) {
            LOGGER.error("Unable to open " + urlAsString);
            throw new RuntimeException("Unable to open " + urlAsString);
        }
        pdfTemplate = JsonUtil.asObject(pdfTemplateAsJson, PDFTemplates.class);

    }

    @Override
    public byte[] createPricingGuide(Contact contact, List<Model> models) throws IOException, DocumentException {

        final Toc tableOfContents = new Toc();

        // add all template pages that should be placed before the Model pages
        byte[] guide = iTextUtil
                .merge(collectPages(pdfTemplate.getBefore(), tableOfContents, Toc.BEFORE_SORT_PREFIX));

        // add the "Models" paragraph to the Toc
        tableOfContents.addEntries(1, Arrays.asList("Models"), null, true, Toc.MODEL_SORT_PREFIX);

        // add the Model pages to the pdf
        guide = iTextUtil.merge(guide, createModelPages(contact, models, tableOfContents));

        // add all template pages that should be placed after the Model Pages
        guide = iTextUtil.merge(guide,
                collectPages(pdfTemplate.getAfter(), tableOfContents, Toc.AFTER_SORT_PREFIX));

        // add empty pages for the Table of Contents
        guide = iTextUtil.merge(guide, addPagesForTableOfContents(tableOfContents));

        // reorder the complete PDF into the final sequence
        guide = iTextUtil.organize(guide, tableOfContents);

        // fix the background for pages without a template (= Table of Contents)
        guide = fixBackground(guide, tableOfContents);

        // enable the links on the Model pages
        guide = enableLinkToWebsite(guide, tableOfContents);

        // add page numbers to all pages
        guide = iTextUtil.setPageNumbers(guide);

        // create a Table of Contents in the bookmark section
        guide = createBookmarks(guide, tableOfContents);

        // create the Table of Contents on the Table of Contents pages
        guide = stampTableOfContents(guide, tableOfContents);

        // personalize the document
        guide = personalize(guide, contact, tableOfContents);

        // set the document properties
        guide = setDocumentProperties(contact, guide);

        return guide;
    }

    private byte[] setDocumentProperties(Contact contact, byte[] guide) throws IOException, DocumentException {

        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {

            final PdfReader reader = new PdfReader(guide);
            final PdfStamper stamper = new PdfStamper(reader, baos);

            final Map<String, String> info = reader.getInfo();

            info.put("Title", documentTitle);
            info.put("Subject", documentTitle);

            info.put("Keywords",
                    "Created for " + contact.getFullName() + ". " + "Currency used : " + contact.getCurrency()
                            + ". " + "Mailed to " + contact.getEmail() + ". " + "Build on "
                            + gitService.getCommitId());

            info.put("Creator", "Roots Software - http://www.roots.be - info@roots.be");
            info.put("Author", "Taconic - http://www.taconic.com");

            stamper.setMoreInfo(info);

            stamper.close();
            reader.close();

            return baos.toByteArray();

        }
    }

    private byte[] createModelPages(Contact contact, List<Model> models, Toc tableOfContents)
            throws IOException, DocumentException {

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Document document = iTextUtil.createNewDocument();
        PdfWriter writer = PdfWriter.getInstance(document, bos);

        document.open();

        // create a PdfPTable for every model (so we can measure the height)
        final List<PdfPTable> modelPageTables = new ArrayList<>();
        Collections.sort(models);
        for (Model model : models) {
            if (model != null) {
                modelPageTables.add(createModelPage(contact, model, writer));
            }
        }

        // put the PdfPTable Models tables on PDF pages (multiple per page if possible)
        byte[] pages = new byte[] {};

        int height = 0;
        List<String> pageNames = new ArrayList<>();

        int i = 0;
        for (PdfPTable modelPageTable : modelPageTables) {

            // create a new pdf page, if necessary
            if (height != 0 && (height + modelPageTable.getTotalHeight()
                    + 20 /* for the line separator */ ) >= iTextUtil.PAGE_HEIGHT) {

                writer.close();
                document.close();
                bos.close();

                byte[] page = bos.toByteArray();
                tableOfContents.addEntries(2, pageNames, page, true,
                        Toc.MODEL_SORT_PREFIX + "___" + IntUtil.format(++i));
                pages = iTextUtil.merge(pages, page);

                height = 0;
                pageNames.clear();
                bos = new ByteArrayOutputStream();
                document = iTextUtil.createNewDocument();
                writer = PdfWriter.getInstance(document, bos);

                document.open();

            } else if (height != 0) {
                // if not the first model on the page, draw a separator

                document.add(new Paragraph(
                        new Chunk(new LineSeparator(.25f, 80f, BaseColor.LIGHT_GRAY, Element.ALIGN_CENTER, -2))));
                document.add(new Paragraph(new Chunk("   ")));
            }

            // rerender the table (with a valid pdfWriter)
            document.add(createModelPage(contact, models.get(modelPageTables.indexOf(modelPageTable)), writer));
            height += modelPageTable.getTotalHeight();
            pageNames.add(models.get(modelPageTables.indexOf(modelPageTable)).getProductNameProcessed());

        }
        writer.close();
        document.close();

        byte[] page = bos.toByteArray();
        tableOfContents.addEntries(2, pageNames, page, true, Toc.MODEL_SORT_PREFIX + "___" + IntUtil.format(++i));
        pages = iTextUtil.merge(pages, page);

        return pages;

    }

    private byte[] addPagesForTableOfContents(Toc tableOfContents) throws IOException, DocumentException {
        final int numberOfPages = tableOfContents.getNumberOfPages();
        tableOfContents.addTocEntry(numberOfPages);
        byte[] pages = new byte[] {};
        for (int i = 0; i < numberOfPages; i++) {
            pages = iTextUtil.merge(pages, iTextUtil.emptyPage());
        }
        return pages;
    }

    private byte[] fixBackground(byte[] pdf, Toc tableOfContents) throws IOException, DocumentException {

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            final byte[] tocTemplate = templateRepository.findOne(pdfTemplate.getTocTemplate().getUrl());
            final byte[] modelPageTemplate = templateRepository.findOne(pdfTemplate.getModel().getUrl());

            final Image tocBackgroundImage = iTextUtil.getImageFromPdf(tocTemplate);
            tocBackgroundImage.setAbsolutePosition(0, 0);

            final Image modelPageBackgroundImage = iTextUtil.getImageFromPdf(modelPageTemplate);
            modelPageBackgroundImage.setAbsolutePosition(0, 0);

            final PdfReader reader = new PdfReader(pdf);
            final PdfStamper stamper = new PdfStamper(reader, bos);

            final PdfContentByte tocContent = stamper.getUnderContent(tableOfContents.getFirstPageOfToc());
            tocContent.addImage(tocBackgroundImage, 612, 0, 0, 792, 0, 0);

            for (int pageNumber = tableOfContents.getFirstPageOfToc() + 1; pageNumber <= tableOfContents
                    .getLastPageNumberOfModelPages(); pageNumber++) {

                final PdfContentByte content = stamper.getUnderContent(pageNumber);
                content.addImage(modelPageBackgroundImage, 612, 0, 0, 792, 0, 0);

            }

            stamper.close();
            reader.close();

            return bos.toByteArray();
        }

    }

    // combines a list of pages together
    private byte[] collectPages(List<Template> pages, Toc tableOfContents, String sortPrefix)
            throws IOException, DocumentException {
        byte[] pdf = new byte[] {};
        int i = 1;
        for (Template pdfTemplate : pages) {
            if (!pdfTemplate.isTocTemplate()) {
                final byte[] template = templateRepository.findOne(pdfTemplate.getUrl());
                pdf = iTextUtil.merge(pdf, template);
                tableOfContents.addEntries(1, Arrays.asList(pdfTemplate.getName()), template, pdfTemplate.isToc(),
                        sortPrefix + "___" + (i++));
            }
        }
        return pdf;
    }

    private PdfPTable createModelPage(Contact contact, Model model, PdfWriter pdfWriter)
            throws IOException, DocumentException {

        final PdfPTable pdfPTable = new PdfPTable(new float[] { 40f, 1f, 59f });
        pdfPTable.setTotalWidth(iTextUtil.PAGE_SIZE.getWidth());
        pdfPTable.addCell(cell(buildModelDetailSection(model, pdfWriter)));
        pdfPTable.addCell(cell(new Paragraph()));
        pdfPTable.addCell(cell(buildModelPricingTables(contact, model)));
        return pdfPTable;

    }

    private Phrase processHtmlCodes(String name, Font baseFont, Font symbol) {

        final Font italicFont = new Font(baseFont);
        italicFont.setStyle(Font.FontStyle.ITALIC.getValue());

        final Font normalFont = new Font(baseFont);

        Font usedFont = normalFont;

        final Phrase phrase = new Phrase();

        if (!StringUtils.isEmpty(name)) {

            for (String[] alphabet : GreekAlphabet.getAlphabet()) {
                name = name.replaceAll(alphabet[0], DELIMETER + alphabet[0] + DELIMETER);
            }
            name = name.replaceAll("<sup>|<SUP>", DELIMETER + "<sup>");
            name = name.replaceAll("</sup>|</SUP>", DELIMETER);
            name = name.replaceAll("<i>|<I>|<em>|<EM>", DELIMETER + "<i>");
            name = name.replaceAll("</i>|</I>|</em>|</EM>", DELIMETER + "</i>");

            final String[] tokens = name.split(DELIMETER);
            for (String token : tokens) {

                String text = token;
                if (text.startsWith("<i>")) {
                    usedFont = italicFont;
                    text = text.substring(3);
                } else if (text.startsWith("</i>")) {
                    usedFont = normalFont;
                    text = text.substring(4);
                }

                usedFont.setSize(baseFont.getSize());

                if (text.startsWith("&")) {
                    final char replacement = GreekAlphabet.getReplacement(text);
                    if (!Character.isWhitespace(replacement)) {
                        phrase.add(SpecialSymbol.get(replacement, symbol));
                    } else {
                        phrase.add(new Chunk(text, usedFont));
                    }
                } else if (text.startsWith("<sup>")) {

                    final Font superScriptFont = new Font(usedFont);
                    superScriptFont.setSize(baseFont.getSize() - 1.5f);

                    final Chunk superScript = new Chunk(text.substring(5), superScriptFont);
                    superScript.setTextRise(4f);
                    phrase.add(superScript);

                } else {
                    phrase.add(new Chunk(text, usedFont));
                }

            }
        }

        return phrase;
    }

    private PdfPTable buildModelPricingTables(Contact contact, Model model) {

        final PdfPTable table = new PdfPTable(1);

        final List<Pricing> validPricings = model.validPricings(contact);

        if (CollectionUtils.isEmpty(validPricings)) {
            final Chunk chunk = new Chunk("Contact us for pricing on this model", iTextUtil.getFontContactUs());
            chunk.setAction(new PdfAction(contactUsUrl));
            table.addCell(cell(new Phrase(chunk)));
        } else {
            for (Pricing pricing : validPricings) {
                table.addCell(cell(buildModelPricingTable(pricing)));
            }
        }

        return table;

    }

    private PdfPTable buildModelPricingTable(Pricing pricing) {
        final PdfPTable pricingTable = new PdfPTable(pricing.getNumberOfHeaderItems());

        pricingTable.addCell(cell(new Paragraph(pricing.getCategory(), iTextUtil.getFontModelCategory()),
                pricing.getNumberOfHeaderItems()));
        pricingTable.addCell(cell(new Paragraph(" "), pricing.getNumberOfHeaderItems()));

        if (pricing.isQuantities()) {
            pricingTable.addCell(cellH(new Paragraph("Quantity", iTextUtil.getFontModelPricingTitle())));
        }
        if (pricing.isAge()) {
            pricingTable.addCell(cellH(new Paragraph("Age (weeks)", iTextUtil.getFontModelPricingTitle())));
        }
        if (pricing.isMale()) {
            pricingTable.addCell(cellH(new Paragraph("Male", iTextUtil.getFontModelPricingTitle())));
        }
        if (pricing.isFemale()) {
            pricingTable.addCell(cellH(new Paragraph("Female", iTextUtil.getFontModelPricingTitle())));
        }
        boolean invert = false;
        for (Line line : pricing.getLines()) {
            if (pricing.isQuantities()) {
                pricingTable.addCell(
                        cellD(new Paragraph(line.getQuantity(), iTextUtil.getFontModelPricingData()), invert));
            }
            if (pricing.isAge()) {
                pricingTable
                        .addCell(cellD(new Paragraph(line.getAge(), iTextUtil.getFontModelPricingData()), invert));
            }
            if (pricing.isMale()) {
                pricingTable
                        .addCell(cellD(new Paragraph(line.getMale(), iTextUtil.getFontModelPricingData()), invert));
            }
            if (pricing.isFemale()) {
                pricingTable.addCell(
                        cellD(new Paragraph(line.getFemale(), iTextUtil.getFontModelPricingData()), invert));
            }
            invert = !invert;
        }

        if (!StringUtils.isEmpty(pricing.getMessage())) {
            pricingTable.addCell(cell(new Paragraph(pricing.getMessage(), iTextUtil.getFontModelPricingMessage()),
                    pricing.getNumberOfHeaderItems()));
        }
        pricingTable.addCell(cell(new Paragraph(" "), pricing.getNumberOfHeaderItems()));

        return pricingTable;
    }

    private PdfPTable buildModelDetailSection(Model model, PdfWriter pdfWriter)
            throws IOException, BadElementException {

        final Phrase p = processHtmlCodes(model.getProductNameProcessed(), iTextUtil.getFontModelTitle(),
                iTextUtil.getFontModelSymbol());
        for (Chunk c : p.getChunks()) {
            c.setAction(new PdfAction(model.getUrl()));
        }

        final StringBuilder strAppList = new StringBuilder();
        for (String application : model.getApplicationsSorted()) {
            if (strAppList.length() != 0) {
                strAppList.append(", ");
            }
            strAppList.append(application);
        }

        final PdfPTable table = new PdfPTable(1);

        table.addCell(cell(p));

        table.addCell(createRow("Model Number", model.getModelNumber(), null));
        table.addCell(createRow("Animal Type", model.getAnimalType(), null));
        table.addCell(createRow("Nomenclature", model.getNomenclatureParsed(), null));
        table.addCell(createRow("Application(s)", strAppList.toString(), null));
        table.addCell(createRow("Health Report", model.getHealthReport(), model.getHealthReport()));
        table.addCell(createRow("Species", model.getSpecies(), null));

        table.addCell(cell(createOrderButton(model)));

        return table;

    }

    private PdfPTable createOrderButton(Model model) throws IOException, BadElementException {

        final Chunk chunk = new Chunk("Order on taconic.com", iTextUtil.getFontButton());
        chunk.setAction(
                new PdfAction("http://www.taconic.com/start-an-order?modelNumber=" + model.getModelNumber()));

        final PdfPCell cell = cell(new Phrase(chunk));
        cell.setBackgroundColor(iTextUtil.getTaconicRed());
        cell.setHorizontalAlignment(Element.ALIGN_CENTER);
        cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
        cell.setPaddingTop(5);
        cell.setPaddingBottom(8);

        final PdfPTable button = new PdfPTable(new float[] { 80f, 20f });
        button.addCell(cell(new Phrase(" "), 2));
        button.addCell(cell);
        button.addCell(cell(new Phrase(" ")));
        button.addCell(cell(new Phrase(" "), 2));

        return button;
    }

    private PdfPCell createRow(String key, String value, String url) {
        final Paragraph paragraph = new Paragraph();
        paragraph.add(new Chunk(key, iTextUtil.getFontModelKey()));
        paragraph.add(new Chunk(": ", iTextUtil.getFontModelKey()));

        if (value == null) {
            value = "";
        }

        final Phrase valuePhrase = processHtmlCodes(value.trim(), iTextUtil.getFontModelValue(),
                iTextUtil.getFontModelSymbol());
        if (!StringUtils.isEmpty(url)) {
            for (Chunk chunk : valuePhrase.getChunks()) {
                chunk.setAction(new PdfAction(url));
            }
        }
        for (Chunk chunk : valuePhrase.getChunks()) {
            chunk.setLineHeight(13f);
        }
        paragraph.add(valuePhrase);

        final PdfPCell cell = cell(paragraph);
        cell.setPaddingBottom(5f);
        return cell;
    }

    private PdfPCell cell(Phrase p, int colSpan) {
        final PdfPCell cell = cell(p);
        cell.setColspan(colSpan);
        return cell;
    }

    private PdfPCell cell(Phrase p) {
        final PdfPCell cell = new PdfPCell(p);
        cell.setBorder(0);
        return cell;
    }

    private PdfPCell cellD(Phrase p, boolean invert) {
        final PdfPCell cell = new PdfPCell(p);
        cell.setBorder(0);
        cell.setPadding(5f);
        cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
        cell.setHorizontalAlignment(Element.ALIGN_CENTER);
        if (invert) {
            cell.setBackgroundColor(iTextUtil.getSilver());
        }
        return cell;
    }

    private PdfPCell cellH(Phrase p) {
        final PdfPCell cell = new PdfPCell(p);
        cell.setBackgroundColor(iTextUtil.getPurple());
        cell.setPadding(5f);
        cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
        cell.setHorizontalAlignment(Element.ALIGN_CENTER);
        cell.setBorder(0);
        return cell;
    }

    private PdfPCell cell(PdfPTable p) {
        final PdfPCell cell = new PdfPCell(p);
        cell.setBorder(0);
        return cell;
    }

    private byte[] personalize(byte[] pdf, Contact contact, Toc tableOfContents)
            throws IOException, DocumentException {

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            final PdfReader reader = new PdfReader(pdf);
            final PdfStamper stamper = new PdfStamper(reader, bos);

            // stamp some text on first page
            PdfContentByte text = stamper.getOverContent(1);

            text.beginText();

            text.setColorFill(iTextUtil.getFontCoverText().getColor());
            text.setFontAndSize(iTextUtil.getFontCoverText().getBaseFont(), iTextUtil.getFontCoverText().getSize());
            text.showTextAligned(Element.ALIGN_RIGHT, coverTitle1,
                    text.getPdfDocument().getPageSize().getWidth() - 15, 195, 0);
            text.showTextAligned(Element.ALIGN_RIGHT, coverTitle2,
                    text.getPdfDocument().getPageSize().getWidth() - 15, 175, 0);
            text.showTextAligned(Element.ALIGN_RIGHT, contact.getCurrency().getTitlePageDescription(),
                    text.getPdfDocument().getPageSize().getWidth() - 15, 80, 0);

            text.setColorFill(iTextUtil.getFontCoverPricingguide().getColor());
            text.setFontAndSize(iTextUtil.getFontCoverPricingguide().getBaseFont(),
                    iTextUtil.getFontCoverPricingguide().getSize());
            text.showTextAligned(Element.ALIGN_RIGHT, coverTitle3,
                    text.getPdfDocument().getPageSize().getWidth() - 15, 145, 0);

            text.setColorFill(iTextUtil.getFontCoverYear().getColor());
            text.setFontAndSize(iTextUtil.getFontCoverYear().getBaseFont(), iTextUtil.getFontCoverYear().getSize());
            text.showTextAligned(Element.ALIGN_RIGHT, coverTitle4,
                    text.getPdfDocument().getPageSize().getWidth() - 15, 105, 0);

            text.endText();

            // stamp some text on first page of the table of contents page
            final Image logoImage = iTextUtil.getImageFromByteArray(HttpUtil.readByteArray(
                    pdfTemplate.getLogo().getUrl(), defaultService.getUserName(), defaultService.getPassword()));
            final PdfContentByte tocContent = stamper.getOverContent(tableOfContents.getFirstPageOfToc());
            final float resizeRatio = logoImage.getHeight() / 85; // define the desired height of the log
            tocContent.addImage(logoImage, logoImage.getWidth() / resizeRatio, 0, 0,
                    logoImage.getHeight() / resizeRatio, 59, 615);

            text = stamper.getOverContent(tableOfContents.getFirstPageOfToc());

            text.beginText();

            text.setColorFill(iTextUtil.getFontPersonalization().getColor());
            text.setFontAndSize(iTextUtil.getFontPersonalization().getBaseFont(),
                    iTextUtil.getFontPersonalization().getSize());
            text.showTextAligned(Element.ALIGN_LEFT, "Prepared for:", 355, 681, 0);
            text.showTextAligned(Element.ALIGN_LEFT, contact.getFullName(), 355, 662, 0);

            // set company name
            if (!StringUtils.isEmpty(contact.getCompany())) {
                text.showTextAligned(Element.ALIGN_LEFT, contact.getCompany(), 355, 643, 0);
                text.showTextAligned(Element.ALIGN_LEFT, new SimpleDateFormat("MM-dd-yyyy").format(new Date()), 355,
                        624, 0);
            } else {
                text.showTextAligned(Element.ALIGN_LEFT, new SimpleDateFormat("MM-dd-yyyy").format(new Date()), 355,
                        643, 0);
            }

            text.endText();

            final ColumnText ct = new ColumnText(tocContent);
            ct.setSimpleColumn(new Rectangle(55, 517, iTextUtil.PAGE_SIZE.getWidth() - 45, 575));
            final List<Element> elements = HTMLWorker.parseToList(new StringReader(disclaimer), null);
            final Paragraph p = new Paragraph();
            p.setAlignment(Element.ALIGN_JUSTIFIED);
            for (Element element : elements) {
                for (Chunk chunk : element.getChunks()) {
                    chunk.setFont(iTextUtil.getFontDisclaimer());
                }
                p.add(element);
            }
            ct.addElement(p);
            ct.go();

            stamper.close();
            reader.close();
            return bos.toByteArray();

        }

    }

    private byte[] createBookmarks(byte[] pdf, Toc tableOfContents) throws DocumentException, IOException {

        // create the bookmarks
        final List<HashMap<String, Object>> outlines = new ArrayList<>();
        final List<TocEntry> entriesSorted = tableOfContents.getEntriesSorted();
        final List<HashMap<String, Object>> modelBookmarkKids = new ArrayList<>();
        HashMap<String, Object> modelBookmark;

        for (TocEntry tocEntry : entriesSorted) {

            if (tocEntry.isIncludedInToc()) {

                final HashMap<String, Object> bookmark = new HashMap<>();

                String name = tocEntry.getName();
                name = name.replaceAll("<sup>", "");
                name = name.replaceAll("</sup>", "");
                name = name.replaceAll("<i.*?>", "");
                name = name.replaceAll("</i>", "");
                name = GreekAlphabet.replaceGreekHtmlCodesWithUnicode(name);

                bookmark.put("Title", name);
                bookmark.put("Action", "GoTo");
                bookmark.put("Page", String.format("%d Fit", tocEntry.getFinalPageNumber()));
                if (tocEntry.getLevel() == 1) {
                    outlines.add(bookmark);
                } else {
                    modelBookmarkKids.add(bookmark);
                }

                if (tocEntry.isModelHeader()) {
                    modelBookmark = bookmark;
                    modelBookmark.put("Open", true);
                    modelBookmark.put("Kids", modelBookmarkKids);
                }

            }

        }

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
            final PdfReader reader = new PdfReader(pdf);
            final PdfStamper stamper = new PdfStamper(reader, bos);

            stamper.setOutlines(outlines);
            stamper.close();
            reader.close();

            return bos.toByteArray();
        }

    }

    private byte[] stampTableOfContents(byte[] pdf, Toc tableOfContents) throws IOException, DocumentException {

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            final PdfReader reader = new PdfReader(pdf);
            final PdfStamper stamper = new PdfStamper(reader, bos);

            // stamp the named destinations
            for (int pageNumber = 1; pageNumber <= reader.getNumberOfPages(); pageNumber++) {
                stamper.addNamedDestination("page" + pageNumber, pageNumber,
                        new PdfDestination(PdfDestination.XYZ, 80f, 800f, 0));
            }

            // create the table of contents
            final Chunk tocTitle = new Chunk("TABLE OF CONTENTS", iTextUtil.getFontTocTitle());

            int currentTocPage = tableOfContents.getFirstPageOfToc();
            int firstTocPage = currentTocPage;
            PdfContentByte canvas = stamper.getOverContent(currentTocPage);

            ColumnText.showTextAligned(canvas, Element.ALIGN_LEFT, new Phrase(tocTitle), 55, 470, 0);

            final List<TocEntry> entriesSorted = tableOfContents.getEntriesSorted();
            int tocEntryNumber = 0;
            for (TocEntry tocEntry : entriesSorted) {

                if (tocEntry.isIncludedInToc()) {
                    tocEntryNumber++;

                    // take the right TOC page to stamp the TOC entry on (needed for TOC's with multiple pages)
                    if (tocEntryNumber == getNumberOfItemsPerTocPage(0)
                            || (tocEntryNumber > getNumberOfItemsPerTocPage(0)
                                    && (tocEntryNumber - getNumberOfItemsPerTocPage(0))
                                            % getNumberOfItemsPerTocPage(currentTocPage - firstTocPage) == 0)) {
                        currentTocPage++;
                        canvas = stamper.getOverContent(currentTocPage);
                    }

                    Font font = iTextUtil.getFontToc();
                    if (tocEntry.getLevel() == 1) {
                        font = iTextUtil.getFontTocBold();
                    }

                    final Phrase p = processHtmlCodes(tocEntry.getLevelString() + tocEntry.getName(), font,
                            iTextUtil.getFontTocSymbol());
                    p.add(new Chunk("", iTextUtil.getFontToc()));
                    if (tocEntry.isShowingPageNumber()) {
                        p.add(new Chunk(new DottedLineSeparator()));
                        p.add(new Chunk("  " + String.valueOf(tocEntry.getFinalPageNumber()),
                                iTextUtil.getFontToc()));
                    }

                    for (Chunk chunk : p.getChunks()) {
                        chunk.setAction(PdfAction.gotoLocalPage("page" + tocEntry.getFinalPageNumber(), false));
                    }

                    int y;
                    if (tocEntryNumber < getNumberOfItemsPerTocPage(0)) {
                        y = 460 - (16 * (tocEntryNumber % getNumberOfItemsPerTocPage(0)));
                    } else {
                        y = 680 - (16 * ((tocEntryNumber - getNumberOfItemsPerTocPage(0))
                                % getNumberOfItemsPerTocPage(currentTocPage - firstTocPage)));
                    }

                    final ColumnText ct = new ColumnText(canvas);
                    ct.setSimpleColumn(p, 52, y, 555, 70, 0, Element.ALIGN_JUSTIFIED);
                    ct.go();

                }
            }

            stamper.close();
            reader.close();

            return bos.toByteArray();

        }

    }

    private byte[] enableLinkToWebsite(byte[] pdf, Toc tableOfContents) throws IOException, DocumentException {

        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {

            final PdfReader reader = new PdfReader(pdf);

            final PdfStamper stamper = new PdfStamper(reader, bos);

            for (int i = tableOfContents.getFirstPageOfToc(); i <= tableOfContents
                    .getLastPageNumberOfModelPages(); i++) {

                final Chunk websiteChunk = new Chunk("..................");
                websiteChunk.setAction(new PdfAction(websiteLink));

                ColumnText ct = new ColumnText(stamper.getUnderContent(i));
                ct.setSimpleColumn(335, 10, 400, 35);
                ct.addText(new Phrase(websiteChunk));
                ct.go();

                final Chunk emailChunk = new Chunk(".........................................");
                emailChunk.setAction(new PdfAction(emailLink));

                ct = new ColumnText(stamper.getUnderContent(i));
                ct.setSimpleColumn(240, 10, 330, 35);
                ct.addText(new Phrase(emailChunk));
                ct.go();

            }

            stamper.close();
            reader.close();
            return bos.toByteArray();

        }

    }

    public static int getNumberOfItemsPerTocPage(int pageNumber) {
        return pageNumber == 0 ? 25 : 39;
    }

}