com.songbook.pc.exporter.PdfExporter.java Source code

Java tutorial

Introduction

Here is the source code for com.songbook.pc.exporter.PdfExporter.java

Source

/*
 *  Copyright (c) 2008 - Tomas Janecek.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.songbook.pc.exporter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Queue;

import com.lowagie.text.Chapter;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.Font;
import com.lowagie.text.FontFactory;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Phrase;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.PdfPCell;
import com.lowagie.text.pdf.PdfPTable;
import com.lowagie.text.pdf.PdfPageEventHelper;
import com.lowagie.text.pdf.PdfWriter;
import com.lowagie.text.pdf.codec.PngImage;
import com.lowagie.text.pdf.draw.LineSeparator;
import com.songbook.core.comparator.SongNodeIndexComparator;
import com.songbook.core.model.ChordNode;
import com.songbook.core.model.LineNode;
import com.songbook.core.model.Node;
import com.songbook.core.model.SongBook;
import com.songbook.core.model.SongNode;
import com.songbook.core.model.TextNode;
import com.songbook.core.model.VerseNode;
import com.songbook.core.util.FileIO;
import com.songbook.core.util.SongNodeLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PdfExporter implements Exporter {
    private static final Logger logger = LoggerFactory.getLogger(PdfExporter.class);
    private static final float POINTS_PER_MM = 2.83464567f;

    private final Font songTitleFont;
    private final Font textFont;
    private final Font chordFont;
    private final Paragraph verseSpacing;
    private final SongNodeLoader loader;

    public PdfExporter(SongNodeLoader loader) {
        this.loader = loader;

        // Load fonts
        FontFactory.registerDirectories();
        BaseFont timesFont = null;
        try {
            timesFont = BaseFont.createFont("C:/Windows/Fonts/times.ttf", BaseFont.CP1250, true);
            logger.info("Embedded TTF fonts from C:/Windows/Fonts");
        } catch (Exception ex) {
            try {
                timesFont = BaseFont.createFont(BaseFont.TIMES_ROMAN, BaseFont.CP1250, true);
                logger.info("Embedded default fonts");
            } catch (Exception ex1) {
                logger.error("Failed to load fonts ...");
            }
        }

        // Initialize fonts
        if (timesFont != null) {
            songTitleFont = new Font(timesFont, 14f, Font.BOLD);
            textFont = new Font(timesFont, 11f, Font.NORMAL);
            chordFont = FontFactory.getFont(FontFactory.HELVETICA, BaseFont.CP1250, 11f, Font.BOLD);
        } else {
            songTitleFont = null;
            textFont = null;
            chordFont = null;
        }

        verseSpacing = new Paragraph(" ");
        verseSpacing.setLeading(5f, 0.5f);
    }

    @Override
    public void export(File baseDir, SongBook songBook) {
        if (songTitleFont == null) {
            throw new RuntimeException("Fonts not correcly loaded - cannot generate PDF !");
        }

        // Output
        File outputDir = new File(baseDir, "pdf");
        File outputFile = new File(outputDir, "song-book.pdf");
        File songIndexFile = new File(outputDir, SongNodeLoader.SONG_INDEX_FILE_NAME);
        FileIO.createDirectory(outputDir);

        try {
            // Sort songs alphabetically
            List<SongNode> orderedList = new ArrayList<SongNode>(songBook.getSongNodeList());
            Collections.sort(orderedList, new SongNodeIndexComparator(Locale.getDefault()));

            // Generate PDF - pass 1 (collect page stats)
            PageStats pageStats = generatePDF(orderedList, outputFile);

            // Reorder songs so that 2-page song always starts on even page number
            List<SongNode> reorderedList = reorderList(orderedList, pageStats);

            // Generate PDF - pass 2
            generatePDF(reorderedList, outputFile);

            // Write song index file
            loader.saveSongIndexFile(songIndexFile, reorderedList);

            // Open document
            openPDF(outputFile);
        } catch (IOException ex) {
            throw new RuntimeException("Failed to write to file " + outputFile.getName(), ex);
        } catch (DocumentException ex) {
            throw new RuntimeException("Document exception " + ex.getMessage(), ex);
        }
    }

    private PageStats generatePDF(List<SongNode> songList, File outputFile) throws IOException, DocumentException {
        logger.info("Starting export to PDF file {}.", outputFile.getAbsolutePath());

        // Initialize Writer
        Document document = new Document();
        PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outputFile));
        PageStats pageStats = new PageStats();
        writer.setPageEvent(pageStats);

        // Initialize document
        document.setPageSize(PageSize.A4);
        document.setMargins(35 * POINTS_PER_MM, 10 * POINTS_PER_MM, 7 * POINTS_PER_MM, 7 * POINTS_PER_MM);
        document.setMarginMirroring(true);

        document.open();

        // Add QR codes
        Element qrCodeSection = buildQrCodeSection();
        document.add(qrCodeSection);

        // Line separator
        document.add(verseSpacing);
        document.add(new LineSeparator());
        document.add(verseSpacing);

        // Build TOC
        Chunk tocTitle = new Chunk("SONG BOOK - TABLE OF CONTENTS", songTitleFont);
        tocTitle.setLocalDestination("TOC");
        document.add(new Paragraph(tocTitle));
        for (int i = 0; i < songList.size(); i++) {
            SongNode songNode = songList.get(i);
            int chapterNumber = i + 1;
            Chunk tocEntry = new Chunk(chapterNumber + ". " + songNode.getTitle(), textFont);
            tocEntry.setLocalGoto("SONG::" + chapterNumber);
            document.add(new Paragraph(tocEntry));
        }
        document.newPage();
        pageStats.setSectionLength("TOC", pageStats.getCurrentPage() - 1);

        // Build document
        for (int i = 0; i < songList.size(); i++) {
            // Get song node
            SongNode songNode = songList.get(i);

            // Mark song start
            int songStartPage = pageStats.getCurrentPage();

            // Write song
            document.add(buildChapter(songNode, i + 1));
            document.newPage();

            // Record song length
            pageStats.setSectionLength(songNode.getTitle(), pageStats.getCurrentPage() - songStartPage);
        }

        // Close document
        document.close();

        logger.info("COMPLETED export to PDF file {}.", outputFile.getAbsolutePath());

        return pageStats;
    }

    private List<SongNode> reorderList(List<SongNode> initialList, PageStats pageStats) {
        // Reorder songs so that 2-page song always starts on even page number
        List<SongNode> reorderedList = new ArrayList<SongNode>();
        Queue<SongNode> songQueue = new LinkedList<SongNode>(initialList);
        int currentPage = 1 + pageStats.getSectionLength("TOC");
        while (!songQueue.isEmpty()) {
            // Find the first song that can be placed
            SongNode nextSongNode = null;

            // Find the next song to with length = 1 or any song we are on an even page
            for (Iterator<SongNode> iterator = songQueue.iterator(); iterator.hasNext();) {
                SongNode songNode = iterator.next();

                if ((pageStats.getSectionLength(songNode.getTitle()) == 1) || (currentPage % 2 == 0)) {
                    iterator.remove();
                    nextSongNode = songNode;
                    break;
                }
            }

            // If no song was selected - use the first one in the alphabetical list
            if (nextSongNode == null) {
                nextSongNode = songQueue.poll();
            }

            // Place song
            reorderedList.add(nextSongNode);
            currentPage += pageStats.getSectionLength(nextSongNode.getTitle());
        }

        return reorderedList;
    }

    private void openPDF(File outputFile) {
        logger.info("OPENING PDF ...");
        String osName = System.getProperty("os.name");
        try {
            if (osName.contains("Linux")) {
                Runtime.getRuntime().exec("acroread " + outputFile.getAbsolutePath());
            } else {
                Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + outputFile.getAbsolutePath());
            }
        } catch (Exception ex) {
            logger.warn("Failed to open PDF !", ex);
        }
    }

    private Element buildQrCodeSection() throws IOException, DocumentException {
        // Load images
        Image qrApkImage = PngImage
                .getImage(PdfExporter.class.getResourceAsStream("/export/qr/songbook_apk_qr.png"));
        Image qrPdfImage = PngImage
                .getImage(PdfExporter.class.getResourceAsStream("/export/qr/songbook_pdf_qr.png"));

        PdfPCell cell = new PdfPCell((Phrase) null);
        cell.setBorder(PdfPCell.NO_BORDER);
        cell.setVerticalAlignment(PdfPCell.ALIGN_CENTER);
        cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER);

        PdfPTable table = new PdfPTable(3);
        table.setWidthPercentage(100);
        table.setWidths(new float[] { 1, 2, 1 });

        cell.setImage(qrApkImage);
        table.addCell(cell);
        cell.setImage(null);

        table.addCell(cell);

        cell.setImage(qrPdfImage);
        table.addCell(cell);
        cell.setImage(null);

        cell.setPhrase(new Phrase("APP"));
        table.addCell(cell);
        cell.setPhrase(null);

        table.addCell(cell);

        cell.setPhrase(new Phrase("PDF"));
        table.addCell(cell);
        cell.setPhrase(null);

        return table;
    }

    private Chapter buildChapter(SongNode songNode, int chapterNumber) {
        // Title
        Chunk chapterTitle = new Chunk(songNode.getTitle(), songTitleFont);
        chapterTitle.setLocalDestination("SONG::" + chapterNumber);
        chapterTitle.setLocalGoto("TOC");

        Chapter chapter = new Chapter(new Paragraph(chapterTitle), chapterNumber);
        for (VerseNode verseNode : songNode.getVerseList()) {
            processVerse(verseNode, chapter);
        }
        return chapter;
    }

    private void processVerse(VerseNode verseNode, Chapter chapter) {
        chapter.add(verseSpacing);
        for (LineNode lineNode : verseNode.getLineNodes()) {
            chapter.add(buildLine(lineNode));
        }

    }

    private Paragraph buildLine(LineNode lineNode) {
        Paragraph paragraph = new Paragraph();
        for (Node node : lineNode.getContentList()) {
            if (node instanceof TextNode) {
                TextNode textNode = (TextNode) node;
                paragraph.add(new Chunk(textNode.getText(), textFont));
            } else if (node instanceof ChordNode) {
                ChordNode chordNode = (ChordNode) node;
                Chunk chunk = new Chunk(" " + chordNode.getText() + " ", chordFont);
                chunk.setTextRise(4f);
                paragraph.add(chunk);
                paragraph.setLeading(4f, 1.2f);
            }
        }
        return paragraph;
    }

    private static class PageStats extends PdfPageEventHelper {
        private int currentPage = 1;
        private final Map<String, Integer> map = new HashMap<String, Integer>();

        public int getCurrentPage() {
            return currentPage;
        }

        public void setSectionLength(String songName, int length) {
            map.put(songName, length);
        }

        public int getSectionLength(String songName) {
            return map.get(songName);
        }

        @Override
        public void onEndPage(PdfWriter pdfWriter, Document document) {
            currentPage++;
        }
    }
}