com.amphisoft.epub2pdf.content.XhtmlHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.amphisoft.epub2pdf.content.XhtmlHandler.java

Source

/*
epub2pdf, version 0.5 - Copyright 2010 Brendan C. LeFebvre
    
This file is part of epub2pdf.
    
epub2pdf 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 3 of the License, or
(at your option) any later version.
    
epub2pdf 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 epub2pdf.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.amphisoft.epub2pdf.content;

import static com.amphisoft.util.StringManip.nothingButSpaces;
import static com.amphisoft.util.StringManip.removeLineBreaks;
import static com.amphisoft.util.StringManip.changeLineBreaksToSpaces;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.*;

import org.apache.xml.resolver.Catalog;
import org.apache.xml.resolver.CatalogManager;
import org.apache.xml.resolver.tools.ResolvingXMLReader;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.amphisoft.epub.Epub;
import com.amphisoft.epub.content.XhtmlTags;
import com.amphisoft.epub.metadata.Ncx.NavPoint;
import com.amphisoft.epub2pdf.metadata.TocTreeNode;
import com.amphisoft.epub2pdf.style.CssParser;
import com.amphisoft.epub2pdf.style.CssStyleMap;
import com.amphisoft.epub2pdf.style.StyleSpecText;
import com.amphisoft.logging.LogEvent;
import com.amphisoft.logging.LogEventPublisher;
import com.amphisoft.logging.LogEventSubscriber;
import com.amphisoft.pdf.ITAlignment;
import com.amphisoft.pdf.ITPageSize;
import com.amphisoft.util.jgtree.Tree;
import com.amphisoft.util.jgtree.TreeNode;

import com.lowagie.text.Anchor;
import com.lowagie.text.Chunk;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Element;
import com.lowagie.text.ElementTags;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.List;
import com.lowagie.text.ListItem;
import com.lowagie.text.Paragraph;
import com.lowagie.text.Rectangle;
import com.lowagie.text.RectangleReadOnly;
import com.lowagie.text.TextElementArray;
import com.lowagie.text.html.SAXmyHtmlHandler;
import com.lowagie.text.pdf.PdfDestination;
import com.lowagie.text.pdf.PdfOutline;

import static com.amphisoft.util.Print.*;

/**
 *
 * @author brendanl
 *
 */
public class XhtmlHandler extends SAXmyHtmlHandler implements LogEventPublisher {

    private Collection<LogEventSubscriber> _subscribers = new ArrayList<LogEventSubscriber>();

    protected Chunk currentChunk = null;
    protected Document document;

    protected Stack<Element> stack = new Stack<Element>();;
    protected Stack<SaxElement> saxElementStack = new Stack<SaxElement>();
    protected String currentTag = "";
    protected String previousTag = "";
    protected File xhtmlDir = null;
    protected String currentFile = "";

    protected static int[] FONTSIZES = { 24, 18, 12, 10, 8, 6 };
    protected static float[] FONTMULTIPLIERS = { 2.0f, 1.5f, 1.0f, 0.83f, 0.75f, 0.5f };
    protected static TextFactory textFactory = new TextFactory();
    protected static int currentITextStyle = Font.NORMAL;
    protected static int defaultAlignment = Paragraph.ALIGN_JUSTIFIED;
    protected static Paragraph specialParagraph = null;
    protected static String[] nonPrintingTags = { "title", "style" };
    protected static Rectangle pageSize = ITPageSize.FOXIT_ESLICK;
    protected static float marginTopPt = 4.0f;
    protected static float marginRightPt = 8.0f;
    protected static float marginBottomPt = 4.0f;
    protected static float marginLeftPt = 4.0f;

    private static CssParser cssParser;
    private static Epub sourceEpub = null;
    private static PdfOutline bookmarkRoot = null;
    private static Tree<TocTreeNode> tocTree = null;

    static {
        cssParser = new CssParser();
    }

    // currently-active style declarations

    // some object "current-style"

    private boolean freshParagraph = false;;

    private boolean inStyleTag = false;
    private StringBuilder styleTagContents = null;
    CssStyleMap styleMap;

    @Override
    public void startDocument() {
        styleMap = new CssStyleMap();
    }

    @Override
    public void characters(char[] ch, int start, int length) {

        String content = new String(ch, start, length);

        String abridgedContent;

        if (content.length() <= 20) {
            abridgedContent = content;
        } else {
            abridgedContent = content.substring(0, 10);
            abridgedContent += " ... ";
            int tailStartIndex = content.length() - 10;
            int tailEndIndex = content.length();
            abridgedContent += content.substring(tailStartIndex, tailEndIndex);
        }

        //printlnerr("chars:[" + abridgedContent + "]");

        boolean printThese = true;
        for (String tag : nonPrintingTags) {
            if (tag.equals(currentTag)) {
                printThese = false;
                break;
            }
        }

        if (inStyleTag) {
            if (styleTagContents == null) {
                styleTagContents = new StringBuilder();
            }
            styleTagContents.append(content);
        } else if (printThese) {
            Font font = null;
            if (XhtmlTags.PRE.equals(currentTag) || XhtmlTags.TT.equals(currentTag)) {
                font = textFactory.getDefaultFontMono();
            } else {
                font = textFactory.getCurrentFont();
            }
            if (currentITextStyle != font.getStyle()) {
                font.setStyle(currentITextStyle);
            }

            if (!(XhtmlTags.PRE.equals(currentTag) || XhtmlTags.TT.equals(currentTag))) {
                if (stack.size() > 0 && stack.peek() instanceof Paragraph) {
                    content = changeLineBreaksToSpaces(content);
                } else {
                    content = removeLineBreaks(content);
                }
                if (nothingButSpaces(content)) {
                    SaxElement tagInProgress = saxElementStack.peek();
                    if (!tagInProgress.qName.equals("p")) {
                        content = "";
                    }
                }
                boolean leadingSpace = content.length() >= 1 && content.startsWith(" ") && !freshParagraph;
                //content.charAt(1) != ' ';
                boolean trailingSpace = content.length() > 1 && content.endsWith(" "); //&&
                //content.charAt(content.length()-2) != ' ';
                content = content.trim();

                if (leadingSpace) {
                    content = " " + content;
                }
                if (trailingSpace)
                    content = content + " ";
            }

            if (content.length() > 0) {
                if (currentChunk != null) {
                    if (specialParagraph != null || currentChunk.getFont().compareTo(font) != 0) {
                        pushToStack(currentChunk);
                        currentChunk = null;
                    } else {
                        currentChunk.append(content);
                    }
                } else {
                    if (specialParagraph != null) {
                        specialParagraph.add(new Chunk(content));
                    } else {
                        currentChunk = new Chunk(content, font);
                    }
                }
            }

            freshParagraph = false;
        }
    }

    String stackStatus() {

        if (stack.size() == 0) {
            return "(empty)";
        } else {
            StringBuilder sB = new StringBuilder();
            for (Element e : stack) {
                sB.append(e.getClass().getSimpleName() + " ");
            }
            return sB.toString();
        }
    }

    int saxElemIdCounter = 1;
    int currentSaxElemId;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        /*
        if("ol".equals(qName) || "ul".equals(qName) || "li".equals(qName)) {
           System.err.print(qName + " ");
        }
         */
        currentSaxElemId = saxElemIdCounter;

        Map<String, String> attrMap = new HashMap<String, String>();
        // parse attributes
        for (int ai = 0; ai < attributes.getLength(); ai++) {
            attrMap.put(attributes.getQName(ai), attributes.getValue(ai));
        }
        String idAttr = attrMap.get("id");
        if (idAttr == null) {
            idAttr = "";
        }
        String className = attrMap.get("class");
        if (className == null) {
            className = "";
        }
        SaxElement sE = new SaxElement(qName, saxElemIdCounter++, className, idAttr, currentITextStyle);
        //printlnerr("startElement: " + sE.toString());
        saxElementStack.push(sE);

        try {
            if (attrMap.get("class") != null) {
                String[] elemClasses = attrMap.get("class").split(" ");

                for (String eClass : elemClasses) {

                    StyleSpecText classTextStyles = styleMap.getTextStyleSpecFor(qName, eClass);
                    if (classTextStyles != null) {
                        sE.applyTextStyles(classTextStyles);
                    }

                }
            }
            if (attrMap.get("style") != null) {
                // TODO this needs more thought, and careful tracking of which tags are still open, etc.
                //String styleSource = attrMap.get("style");
                //CssStyleMap styleTagStyles = cssParser.getStylesFromStyleTag(styleSource);
                // ...
            }
            if (sE.textStyles == null) {
                try {
                    int stackSize = saxElementStack.size();
                    if (stackSize > 1) {
                        SaxElement enclosingElement = saxElementStack.elementAt(stackSize - 2);
                        StyleSpecText enclosingSST = enclosingElement.textStyles;
                        if (enclosingSST != null)
                            sE.applyTextStyles(enclosingSST);
                    }
                } catch (Exception e) {
                }
            }

            StyleSpecText currentTextStyles = sE.textStyles;
            if (currentTextStyles != null) {
                if (currentTextStyles.isBold()) {
                    currentITextStyle |= Font.BOLD;
                }
                if (currentTextStyles.isItalic()) {
                    currentITextStyle |= Font.ITALIC;
                }
            }

            //System.err.println("PUSH -> " + saxElementStack);

            previousTag = currentTag;
            currentTag = qName;
            if (document.isOpen()) {
                if (XhtmlTags.NEWLINE.equals(qName)) {
                    if (stack.size() > 0) {
                        TextElementArray currentTEA = (TextElementArray) stack.peek();
                        currentTEA.add(Chunk.NEWLINE);
                    } else if (specialParagraph != null) {
                        specialParagraph.add(Chunk.NEWLINE);
                    }
                }

                updateStack();

                String xmlElementId = attrMap.get("id");

                if (XhtmlTags.ANCHOR.equals(qName)) {
                    //concession to nonconformists...
                    if (xmlElementId == null) {
                        xmlElementId = attrMap.get("name");
                    }
                    Anchor anchor = textFactory.newAnchor();
                    String ref = attrMap.get(XhtmlTags.REFERENCE);

                    if (ref != null) {
                        int aNameStartIdx = ref.lastIndexOf("#") + 1;
                        ref = ref.substring(aNameStartIdx);
                        anchor.setReference(ref);
                    }
                    if (xmlElementId != null) {
                        anchor.setName(xmlElementId);
                    }
                    pushToStack(anchor);
                } else {
                    if (xmlElementId != null) {
                        //flushStack();
                        Anchor dest = textFactory.newAnchor();
                        dest.setName(xmlElementId);
                        pushToStack(dest);
                        //flushStack();
                    }
                    for (int i = 0; i < 6; i++) {
                        if (XhtmlTags.H[i].equals(qName)) {
                            flushStack();
                            freshParagraph = true;
                            currentITextStyle |= Font.BOLD;
                            specialParagraph = textFactory.newHeadline(i + 1);
                            return;
                        }
                    }
                    if ("blockquote".equals(qName)) {
                        flushStack();
                        freshParagraph = true;
                        Paragraph p = textFactory.newParagraph();
                        p.setIndentationLeft(50);
                        p.setIndentationRight(20);
                        p.setAlignment(defaultAlignment);
                        pushToStack(p);
                    } else if (XhtmlTags.PARAGRAPH.equals(qName)) {
                        flushStack();
                        freshParagraph = true;
                        Paragraph p = textFactory.newParagraph();
                        pushToStack(p);
                    } else if (XhtmlTags.DIV.equals(qName)) {
                        if (stack.size() > 0 && stack.peek().getChunks().size() > 0) {
                            flushStack();
                        }
                        if (stack.size() == 0) {
                            Paragraph brandNewParagraph = textFactory.newParagraph();
                            pushToStack(brandNewParagraph);
                            freshParagraph = true;
                        }
                    } else if (XhtmlTags.PRE.equals(qName)) {
                        flushStack();
                        freshParagraph = true;
                        Paragraph p = textFactory.newParagraphPre();
                        pushToStack(p);
                    } else if (XhtmlTags.ORDEREDLIST.equals(qName)) {
                        flushStack();
                        List oList = new List(List.ORDERED, 10);
                        pushToStack(oList);
                    } else if (XhtmlTags.UNORDEREDLIST.equals(qName)) {
                        flushStack();
                        List uList = new List(List.UNORDERED, 10);
                        pushToStack(uList);
                    } else if (XhtmlTags.LISTITEM.equals(qName)) {
                        freshParagraph = true;
                        ListItem listItem = new ListItem();
                        pushToStack(listItem);
                    } else if (XhtmlTags.IMAGE.equals(qName)) {
                        handleImage(attributes);
                    } else if (qName != null && qName.endsWith("image")) {
                        handleSvgImage(attributes);
                    } else if (XhtmlTags.LINK.equals(qName)) {
                        // if it's a stylesheet, parse it & update current-style
                        if ("stylesheet".equals(attrMap.get("rel")) && "text/css".equals(attrMap.get("type"))
                                && attrMap.get("href") != null) {
                            String cssHref = xhtmlDir.getAbsoluteFile().toURI().toString() + attrMap.get("href");
                            CssStyleMap stylesFromLink = cssParser.getStylesFromFileURI(cssHref);
                            if (stylesFromLink != null) {
                                styleMap.updateWith(stylesFromLink);
                            }
                        }
                    } else if (XhtmlTags.STYLE.equals(qName)) {
                        inStyleTag = true;
                    } else if (XhtmlTags.EM.equals(qName) || "I".equals(qName.toUpperCase())) {
                        currentITextStyle |= Font.ITALIC;
                    } else if (XhtmlTags.STRONG.equals(currentTag) || "B".equals(qName.toUpperCase())) {
                        currentITextStyle |= Font.BOLD;
                    }

                }

            } else if (XhtmlTags.BODY.equals(qName)) {
                document.open();
                freshParagraph = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        //printlnerr("leaving startElement " + localName + "; stack: " + stackStatus());
    }

    @Override
    public void endElement(String uri, String localName, String qName) {
        //System.err.println("PRE-POP  -> " + saxElementStack);
        SaxElement closedElement = saxElementStack.pop();
        //System.err.println("POST-POP -> " + saxElementStack);
        currentITextStyle = closedElement.precedingItextStyle;

        //printlnerr("endElement:" + closedElement.toString());
        /*
        if("li".equals(qName)) {
           System.err.print("/" + qName + " ");
        }
            
        if("ol".equals(qName) || "ul".equals(qName)) {
           printlnerr("/" + qName + " ");
        }
         */
        //printlnerr("entering endElement " + localName + "; stack: " + stackStatus());
        try {
            //printlnerr("</" + qName + ">");
            if (document.isOpen()) {
                updateStack();
                for (int i = 0; i < 6; i++) {
                    if (XhtmlTags.H[i].equals(qName)) {
                        //currentITextStyle ^= Font.BOLD;
                        if (specialParagraph != null) {
                            pushToStack(specialParagraph);
                        }
                        flushStack();
                        specialParagraph = null;
                        return;
                    }
                }
                if (XhtmlTags.BLOCKQUOTE.equals(qName) || XhtmlTags.ORDEREDLIST.equals(qName)
                        || XhtmlTags.UNORDEREDLIST.equals(qName) || XhtmlTags.PRE.equals(qName)
                        || XhtmlTags.PARAGRAPH.equals(qName) || XhtmlTags.DIV.equals(qName)) {
                    flushStack();
                } else if (XhtmlTags.TT.equals(qName)) {
                    updateStack();
                } else if (XhtmlTags.LISTITEM.equals(qName)) {
                    ListItem listItem = null;
                    List list = null;
                    try {
                        Element stackTop = (Element) popFromStack();
                        try {
                            listItem = (ListItem) stackTop;
                        } catch (ClassCastException cce) {
                            pushToStack(stackTop);
                        }
                        try {
                            Element stackTop2 = (Element) popFromStack();
                            try {
                                list = (List) stackTop2;
                            } catch (ClassCastException cce2) {
                                pushToStack(stackTop2);
                            }
                        } catch (EmptyStackException e) {
                            //printlnerr("*** EMPTY STACK (List)");
                        }
                    } catch (EmptyStackException e) {
                        //printlnerr("*** EMPTY STACK (ListItem)");
                    } finally {
                        if (listItem != null && list != null)
                            list.add(listItem);
                        if (list != null) {
                            pushToStack(list);
                        }
                    }
                } else if (XhtmlTags.ANCHOR.equals(qName)) {
                    Anchor anchor = null;
                    Element stackTop = null;
                    try {
                        stackTop = (Element) popFromStack();
                        try {
                            anchor = (Anchor) stackTop;
                        } catch (ClassCastException cce) {
                            if (stackTop != null) {
                                pushToStack(stackTop);
                            }
                        }
                        if (anchor != null) {
                            pushToStack(anchor);
                        }
                    } catch (EmptyStackException es) {
                        if (anchor != null) {
                            pushToStack(anchor);
                        }
                    }
                } else if (XhtmlTags.HTML.equals(qName)) {
                    flushStack();
                    if (this.controlOpenClose) {
                        document.close();
                    }
                } else if (XhtmlTags.EM.equals(qName) || "I".equals(qName.toUpperCase())) {
                    //currentITextStyle ^= Font.ITALIC;
                } else if (XhtmlTags.STRONG.equals(currentTag) || "B".equals(qName.toUpperCase())) {
                    //currentITextStyle ^= Font.BOLD;
                } else if (XhtmlTags.STYLE.equals(qName)) {
                    if (styleTagContents != null) {
                        CssStyleMap stylesFromHeadTag = cssParser
                                .getStylesFromStyleTag(styleTagContents.toString());
                        if (stylesFromHeadTag != null) {
                            styleMap.updateWith(stylesFromHeadTag);
                        }

                        styleTagContents = null;
                    }
                    inStyleTag = false;
                }

            }

            //printlnerr("leaving endElement " + localName + "; stack: " + stackStatus());
        } catch (Exception e) {
            e.printStackTrace();
        }

        /*
        if (saxElementStack.size()>0) {
            
           StyleSpecText revertStyles = saxElementStack.peek().textStyles;
           if (revertStyles != null) {
        currentITextStyle = Font.NORMAL;
        if (revertStyles.isBold()) {
           currentITextStyle |= Font.BOLD;
        }
        if (revertStyles.isItalic()) {
           currentITextStyle |= Font.ITALIC;
        }
           }
           else {
        currentITextStyle = Font.NORMAL;
           }
        }
        */
    }

    @SuppressWarnings("unused")
    private void debugElementStack(Stack<Element> elemStack) {
        for (Element elem : elemStack) {
            debugElementNoBreak(elem);
            printerr(" | ");
        }
        printlnerr("");
    }

    /**
     * Flushes the stack, adding all objects in it to the document.
     */
    private void flushStack() {
        //printlnerr("entering flushStack; stack: " + stackStatus());
        Stack<Element> reverseStack = new Stack<Element>();
        try {
            //printlnerr("*** FLUSH COMMENCE ***");
            //debugElementStack(stack);
            while (stack.size() > 0) {
                Element element = null;
                try {
                    element = (Element) popFromStack();
                    if (!(element instanceof TextElementArray)) {
                        System.err.print("");
                        printlnerr("Orphaned element: [" + element.toString() + "]");
                    } else if (element instanceof Anchor) {
                        reverseStack.push(element);
                    } else {
                        TextElementArray previous = (TextElementArray) popFromStack();
                        if (previous != null) {
                            if (previous instanceof Anchor) {
                                reverseStack.push(element);
                                reverseStack.push(previous);
                            } else {
                                previous = appendToTEA(previous, element);
                                pushToStack(previous);
                            }
                        } else {
                            pushToStack(element);
                        }
                    }

                } catch (EmptyStackException es) {
                    if (element != null) {
                        reverseStack.push(element);
                    }
                }
            }
            //printlnerr("*** FLUSH ***");
        } catch (Exception e) {
            e.printStackTrace();
            printerr("");
        }

        //printlnerr("*** ADDING TO PDF ***");
        //debugElementStack(reverseStack);

        try {
            printerr("");
            while (reverseStack.size() > 0) {
                Element elemToAdd = reverseStack.pop();
                if (elemToAdd instanceof TextElementArray) {
                    checkTextElementArrayForTocAnchors(document, (TextElementArray) elemToAdd);
                }
                addToDocument(elemToAdd);

            }
        } catch (DocumentException docEx) {
            docEx.printStackTrace();
        }

    }

    private TextElementArray appendToTEA(TextElementArray previous, Element element) {
        //debugAppendToTEA(previous,element);
        @SuppressWarnings("unused")
        boolean success = previous.add(element);
        //debugAppendedTEA(previous,success);
        return previous;
    }

    @SuppressWarnings("unused")
    private void debugAppendToTEA(TextElementArray tea, Element element) {
        printerr("Appending to TEA: ");
        debugElementNoBreak(tea);
        printerr(" + ");
        debugElement(element);
    }

    @SuppressWarnings("unused")
    private void debugAppendedTEA(TextElementArray tea, boolean success) {
        printerr("Append to TEA ");
        printerr(success ? "succeeded: " : "FAILED: ");
        printerr("TEA now: ");
        debugElement(tea);
    }

    private void checkTextElementArrayForTocAnchors(Document doc, TextElementArray tea) throws DocumentException {
        if (tea instanceof Anchor) {
            Anchor a = (Anchor) tea;
            addAnchorToBookmarks(a);
        } else {
            if (tea.type() == Element.PARAGRAPH) {
                Paragraph p = (Paragraph) tea;
                for (int i = 0; i < p.size(); i++) {
                    Object o = p.get(i);
                    if (o instanceof TextElementArray) {
                        checkTextElementArrayForTocAnchors(doc, (TextElementArray) o);
                    }
                }
            }
        }
    }

    private void addToBookmarks(String currentFile, String aName) { // curFileCP, aName
        try {
            StringBuilder lookupSB = new StringBuilder();
            String currentFileBaseName = new File(currentFile).getName();
            String currentFileEpubRooted = null;
            for (String spineRef : sourceEpub.getOpf().spineHrefs()) {
                if (spineRef.endsWith(currentFileBaseName)) {
                    currentFileEpubRooted = spineRef;
                    break;
                }
            }
            if (currentFileEpubRooted == null) {
                //throw new Exception();
            }
            lookupSB.append(currentFileEpubRooted);
            if (aName != null && !aName.isEmpty()) {
                lookupSB.append('#');
                lookupSB.append(aName);
            }
            String lookup = lookupSB.toString();
            NavPoint nP = sourceEpub.getNcx().findNavPoint(lookup);
            TreeNode<TocTreeNode> npNode = TocTreeNode.findInTreeByNavPoint(tocTree, nP);
            if (nP != null) {
                document.newPage();

                PdfOutline pdfOutlineParent = bookmarkRoot;
                if (npNode != null) {
                    TreeNode<TocTreeNode> parent = npNode.getParent();
                    if (parent != null) {
                        TocTreeNode parentTTN = parent.getValue();
                        if (parentTTN != null && parentTTN.getPdfOutline() != null) {
                            pdfOutlineParent = parentTTN.getPdfOutline();
                        }
                    }
                }

                PdfDestination here = new PdfDestination(PdfDestination.FIT);
                PdfOutline pdfTocEntry = new PdfOutline(pdfOutlineParent, here, nP.getNavLabelText());
                if (npNode != null) {
                    npNode.getValue().setPdfDestination(here);
                    npNode.getValue().setPdfOutline(pdfTocEntry);
                }
            } else {
                //throw new Exception("NCX lookup fail: " + lookup);
            }
        } catch (Exception e) {
            //printlnerr("Failed to update PDF bookmarks with Anchor " + aName + ": " + e.getMessage());
        }
    }

    private void addAnchorToBookmarks(Anchor a) {
        if (sourceEpub != null && bookmarkRoot != null && a.getReference() == null && a.getName() != null) {
            String aName = a.getName();
            addToBookmarks(currentFile, aName);
        }
    }

    private java.util.List<String> getNoNewSpaceTagsList() {
        String[] noNewSpaceTags = { "a", "span", "em", "strong", "small", "br" };
        java.util.List<String> noNewSpaceTagsList = new ArrayList<String>();
        for (String tag : noNewSpaceTags) {
            noNewSpaceTagsList.add(tag);
        }
        return noNewSpaceTagsList;

    }

    /**
     * If the current Chunk is not null, its constituents are forwarded to the stack and it is then made
     * null.
     */
    private void updateStack() {
        //printlnerr("entering updateStack; stack: " + stackStatus());
        java.util.List<String> noNewSpaceTagsColl = getNoNewSpaceTagsList();

        if (currentChunk != null) {
            TextElementArray current;
            try {
                current = (TextElementArray) popFromStack();
                if ((!(current instanceof Paragraph) || !((Paragraph) current).isEmpty())
                        && !(noNewSpaceTagsColl.contains(currentTag))) {
                    current = appendToTEA(current, new Chunk(" "));
                }
            } catch (EmptyStackException ese) {
                current = textFactory.newParagraph();
            }
            //printlnerr("*** CHUNK {" + currentChunk.getContent() + "}");
            current.add(currentChunk);
            pushToStack(current);
            currentChunk = null;
        }
        //printlnerr("leaving updateStack; stack: " + stackStatus());
    }

    private void handleImage(String url, String alt, String borderWidth, String alignment)
            throws DocumentException {
        float marginTopPt = document.topMargin();
        float marginBottomPt = document.bottomMargin();
        float marginLeftPt = document.leftMargin();
        float marginRightPt = document.rightMargin();

        if (url == null)
            return;
        Image img = null;
        String imgSimpleName = "";
        try {
            File imgFile = new File(xhtmlDir, url);
            String imgPath = imgFile.getCanonicalPath();
            imgSimpleName = imgFile.getName();
            img = Image.getInstance(imgPath);
            if (alt == null) {
                alt = imgSimpleName;
            }
            img.setAlt(alt);
        } catch (Exception e) {
            printlnerr("epub2pdf: problem adding image " + imgSimpleName + ": " + e.getMessage());
            return;
        }
        if (borderWidth != null) {
            int border = Integer.parseInt(borderWidth);
            if (border == 0) {
                img.setBorder(Image.NO_BORDER);
            } else {
                img.setBorder(Image.BOX);
                img.setBorderWidth(border);
            }
        }

        if (alignment != null) {
            int align = Image.DEFAULT;
            if (ElementTags.ALIGN_LEFT.equalsIgnoreCase(alignment))
                align = Image.LEFT;
            else if (ElementTags.ALIGN_RIGHT.equalsIgnoreCase(alignment))
                align = Image.RIGHT;
            else if (ElementTags.ALIGN_MIDDLE.equalsIgnoreCase(alignment))
                align = Image.MIDDLE;
            img.setAlignment(align | Image.TEXTWRAP);
        } else {
            img.setAlignment(Image.MIDDLE);
        }

        Rectangle pageRect = document.getPageSize();
        float verticalMarginTotal = marginTopPt + marginBottomPt;
        float horizontalMarginTotal = marginLeftPt + marginRightPt;

        float imgMaxWidth = pageRect.getWidth() - horizontalMarginTotal - 2;
        float imgMaxHeight = pageRect.getHeight() - verticalMarginTotal - 2;

        float imgOrigWidth = img.getWidth();
        float imgOrigHeight = img.getHeight();

        if (imgOrigHeight > imgMaxHeight || imgOrigWidth > imgMaxWidth) {
            img.scaleToFit(imgMaxWidth, imgMaxHeight);
        }
        boolean addOK = addToDocument(img);
        if (!addOK) {
            System.err.println("** addToDocument(" + img.getUrl() + ") returned false");
        }

    }

    /**
     * Handles the attributes of an IMG tag.
     *
     * @param attributes
     * @throws DocumentException
     */
    private void handleImage(Attributes attributes) throws DocumentException {
        String url = attributes.getValue(XhtmlTags.URL);
        String alt = attributes.getValue(XhtmlTags.ALT);
        String borderWidth = attributes.getValue(XhtmlTags.BORDERWIDTH);
        String alignment = attributes.getValue(XhtmlTags.ALIGN);
        handleImage(url, alt, borderWidth, alignment);
    }

    /**
     * Handles the attributes of an svg.image tag.
     * @param attributes
     * @throws DocumentException
     */
    private void handleSvgImage(Attributes attributes) throws DocumentException {
        String url = attributes.getValue("xlink:href");
        handleImage(url, null, null, null);
    }

    public XhtmlHandler(String xhtml, Document docInProgress)
            throws MalformedURLException, IOException, SAXException {
        super(docInProgress);
        currentFile = xhtml;
        this.controlOpenClose = false;
        document = docInProgress;

        parseXhtml(xhtml);
    }

    void parseXhtml(String xhtml) throws MalformedURLException, IOException, SAXException {

        File xhtmlFile = new File(xhtml);

        //printlnerr("Processing " + xhtmlFile.getName());

        xhtmlDir = xhtmlFile.getParentFile();

        CatalogManager.getStaticManager().setIgnoreMissingProperties(true);
        ResolvingXMLReader reader = new ResolvingXMLReader();
        Catalog catalog = reader.getCatalog();
        catalog.parseCatalog("etc/xhtml/catalog.xml");
        reader.setContentHandler(this);
        InputSource iS = new InputSource(new FileInputStream(xhtml));
        reader.parse(iS);
    }

    public static void process(String xhtml, Document doc) {
        try {
            new XhtmlHandler(xhtml, doc);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    static String getAbridgedContents(Element elem) {
        String content = "";
        if (elem != null)
            content += elem.toString();
        int len = content.length();
        if (len > 25) {
            StringBuilder sB = new StringBuilder();
            sB.append(content.substring(0, 10));
            sB.append(" ... ");
            sB.append(content.substring(len - 10));
            content = sB.toString();
        }
        if (elem instanceof Anchor) {
            Anchor elemAnchor = (Anchor) elem;
            StringBuilder sB = new StringBuilder();
            sB.append('_');
            sB.append(elemAnchor.getName());
            if (elemAnchor.getReference() != null) {
                sB.append("->");
                sB.append(elemAnchor.getReference());
            }
            sB.append(":");
            sB.append(content);
            content = sB.toString();
        }
        return content;
    }

    static class SaxElement {
        final String qName;
        final int id;
        final String className;
        final String idAttr;
        final int precedingItextStyle;
        StyleSpecText textStyles;

        SaxElement(String q, int i, String c, String idA, int preStyle) {
            qName = q;
            id = i;
            className = c;
            idAttr = idA;
            precedingItextStyle = preStyle;
        }

        void applyTextStyles(StyleSpecText sst) {
            textStyles = new StyleSpecText(sst);
        }

        @Override
        public String toString() {
            StringBuilder sB = new StringBuilder(qName);
            if (className != null && className.length() > 0) {
                sB.append(":");
                sB.append(className);
            }
            sB.append("_");
            sB.append(id);
            if (idAttr != null && idAttr.length() > 0) {
                sB.append("(");
                sB.append(idAttr);
                sB.append(")");
            }
            if (textStyles != null) {
                sB.append('_');
                sB.append(textStyles.toString());
            }
            return sB.toString();
        }
    }

    public static void setDefaultPageSize(float widthPt, float heightPt) {
        pageSize = new RectangleReadOnly(widthPt, heightPt);
    }

    public static void setDefaultAlignment(int itextAlignmentCode) {
        defaultAlignment = itextAlignmentCode;
        TextFactory.setDefaultITAlignment(ITAlignment.getValueForInt(itextAlignmentCode));
    }

    public static void setSourceEpub(Epub epubIn) {
        sourceEpub = epubIn;
    }

    public static void setBookmarkRoot(PdfOutline outline) {
        bookmarkRoot = outline;
    }

    public static void setTocTree(Tree<TocTreeNode> tree) {
        tocTree = tree;
    }

    private Element pushToStack(Element elem) {
        //debugPushing(elem);
        if (elem instanceof Anchor && stack.size() > 0) {
            Element stackTop = stack.peek();
            if (stackTop instanceof Paragraph) {
                ((Paragraph) stackTop).add(elem);
                return elem;
            }
        }
        return stack.push(elem);
    }

    private Element popFromStack() throws EmptyStackException {
        //debugPopping(stack.peek());
        return stack.pop();
    }

    private boolean addToDocument(Element elem) throws DocumentException {
        //debugAdding(elem);
        return document.add(elem);
    }

    @SuppressWarnings("unused")
    private void debugPushing(Element elem) {
        printerr("Pushing: ");
        debugElement(elem);
    }

    @SuppressWarnings("unused")
    private void debugPopping(Element elem) {
        printerr("Popping: ");
        debugElement(elem);
    }

    @SuppressWarnings("unused")
    private void debugAdding(Element elem) {
        printerr("Adding: ");
        debugElement(elem);
    }

    private void debugElement(Element elem) {
        printerr(getClassOf(elem));
        printlnerr(getAbridgedContents(elem));
    }

    private void debugElementNoBreak(Element elem) {
        printerr(getClassOf(elem));
        printerr(getAbridgedContents(elem));
    }

    private String getClassOf(Element elem) {
        if (elem == null) {
            return "<null>";
        } else {
            Object elemObj = (Object) elem;
            return elemObj.getClass().getSimpleName();
        }
    }

    @Override
    public boolean subscribe(LogEventSubscriber sub) {
        return _subscribers.add(sub);
    }

    @Override
    public boolean unsubscribe(LogEventSubscriber sub) {
        return _subscribers.remove(sub);
    }

    void notifySubscribers(LogEvent ev) {
        for (LogEventSubscriber sub : _subscribers) {
            sub.notify(ev);
        }
    }
}