com.chaschev.itext.ColumnTextBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.chaschev.itext.ColumnTextBuilder.java

Source

/*
 * Copyright (C) 2013 Afoundria.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.chaschev.itext;

import com.chaschev.chutils.util.Exceptions;
import com.google.common.base.Preconditions;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Iterators;
import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.interfaces.IAccessibleElement;

import javax.annotation.Nullable;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * User: ACHASCHEV
 * Date: 7/11/13
 * Time: 3:05 PM
 */
public class ColumnTextBuilder {
    public static final SimpleGrowStrategy DEFAULT_GROW_STRATEGY = new SimpleGrowStrategy(10.0);

    private final ITextBuilder iTextBuilder;
    ColumnText columnText;
    private Rectangle simpleColumnRectangle;

    protected float pageBottom;
    protected float pageTop;
    private boolean frozen;

    private ColumnTextBuilder storedState;

    public ColumnTextBuilder(ITextBuilder iTextBuilder) {
        this.iTextBuilder = iTextBuilder;
    }

    public ColumnTextBuilder newColumnText(PdfContentByte canvas) {
        columnText = new ColumnText(canvas);
        return this;
    }

    public int getLinesWritten() {
        return columnText.getLinesWritten();
    }

    public void setCanvases(PdfContentByte[] canvases) {
        columnText.setCanvases(canvases);
    }

    public PdfContentByte getCanvas() {
        return columnText.getCanvas();
    }

    public boolean hasMoreText(int status) {
        return ColumnText.hasMoreText(status);
    }

    /**
     * Avoid using this non-API method.
     */
    public boolean hasMoreText() {
        try {
            final Object compositeElements = compositeElementsField.get(columnText);

            return getVersion() != 0 || (compositeElements != null && !((List) compositeElements).isEmpty());
        } catch (IllegalAccessException e) {
            throw Exceptions.runtime(e);
        }
    }

    public boolean zeroHeightElement() {
        return columnText.zeroHeightElement();
    }

    public static float fitText(Font font, String text, Rectangle rect, float maxFontSize, int runDirection) {
        return ColumnText.fitText(font, text, rect, maxFontSize, runDirection);
    }

    public void setCanvas(PdfContentByte canvas) {
        columnText.setCanvas(canvas);
    }

    public ColumnTextBuilder addElement(Element element) {
        columnText.addElement(element);
        return this;
    }

    public int getRowsDrawn() {
        return columnText.getRowsDrawn();
    }

    public ColumnTextBuilder setACopy(ColumnTextBuilder source) {
        clearContent(); //content is saved after setACopy

        setSimpleColumn(new Rectangle(source.simpleColumnRectangle));

        columnText.setACopy(source.columnText);

        this.pageBottom = source.pageBottom;
        this.pageTop = source.pageTop;

        return this;
    }

    public ColumnTextBuilder copyContentFrom(ColumnTextBuilder source) {
        if (source == this) {
            return this;
        }

        final Rectangle temp = simpleColumnRectangle;

        setACopy(source);

        setSimpleColumn(temp);

        return this;
    }

    public boolean getInheritGraphicState() {
        return columnText.getInheritGraphicState();
    }

    public void clearChunks() {
        columnText.clearChunks();
    }

    public float getLeading() {
        return columnText.getLeading();
    }

    public ColumnTextBuilder setColumns(float[] leftLine, float[] rightLine) {
        columnText.setColumns(leftLine, rightLine);
        simpleColumnRectangle = null;
        return this;
    }

    public float getFilledWidth() {
        return columnText.getFilledWidth();
    }

    public ColumnTextBuilder setLeading(float leading) {
        columnText.setLeading(leading);
        return this;
    }

    public float getSpaceCharRatio() {
        return columnText.getSpaceCharRatio();
    }

    public int getRunDirection() {
        return columnText.getRunDirection();
    }

    public PdfContentByte[] getCanvases() {
        return columnText.getCanvases();
    }

    public int getArabicOptions() {
        return columnText.getArabicOptions();
    }

    public ColumnTextBuilder setSpaceCharRatio(float spaceCharRatio) {
        columnText.setSpaceCharRatio(spaceCharRatio);
        return this;
    }

    public ColumnTextBuilder setSimpleColumn(float llx, float lly, float urx, float ury) {
        return setSimpleColumn(new Rectangle(llx, lly, urx, ury));
    }

    public int go(boolean simulate, IAccessibleElement elementToGo) throws DocumentException {
        Preconditions.checkArgument(!frozen, "can't go when frozen");
        return columnText.go(simulate, elementToGo);
    }

    public ColumnTextBuilder addText(Chunk chunk) {
        columnText.addText(chunk);
        return this;
    }

    public static boolean isAllowedElement(Element element) {
        return ColumnText.isAllowedElement(element);
    }

    public ColumnTextBuilder setText(Phrase phrase) {
        columnText.setText(phrase);
        return this;
    }

    public ColumnTextBuilder clearContent() {
        return setText(new Phrase(""));
    }

    public boolean isAdjustFirstLine() {
        return columnText.isAdjustFirstLine();
    }

    public float getExtraParagraphSpace() {
        return columnText.getExtraParagraphSpace();
    }

    public float getYLine() {
        return columnText.getYLine();
    }

    public void setFilledWidth(float filledWidth) {
        columnText.setFilledWidth(filledWidth);
    }

    public void setIndent(float indent) {
        columnText.setIndent(indent);
    }

    public void setAdjustFirstLine(boolean adjustFirstLine) {
        columnText.setAdjustFirstLine(adjustFirstLine);
    }

    public float getMultipliedLeading() {
        return columnText.getMultipliedLeading();
    }

    public float getLastX() {
        return columnText.getLastX();
    }

    public float getIndent() {
        return columnText.getIndent();
    }

    public ColumnTextBuilder goGo() throws DocumentException {
        while (ColumnText.hasMoreText(go())) {
            iTextBuilder.document.newPage();

            simpleColumnRectangle = new Rectangle(simpleColumnRectangle);

            simpleColumnRectangle.setBottom(pageBottom);
            simpleColumnRectangle.setTop(pageTop);

            columnText.setSimpleColumn(simpleColumnRectangle);

            iTextBuilder.defaultColumnTextSettings.apply(this);
        }

        return this;
    }

    public int go() {
        try {
            Preconditions.checkArgument(!frozen, "can't go when frozen");

            return columnText.go();
        } catch (DocumentException e) {
            throw Exceptions.runtime(e);
        }
    }

    public void setExtraParagraphSpace(float extraParagraphSpace) {
        columnText.setExtraParagraphSpace(extraParagraphSpace);
    }

    public float getCurrentLeading() {
        return columnText.getCurrentLeading();
    }

    public int go(boolean simulate) {
        try {
            Preconditions.checkArgument(!frozen, "can't go when frozen");
            return columnText.go(simulate);
        } catch (DocumentException e) {
            throw Exceptions.runtime(e);
        }
    }

    public void setInheritGraphicState(boolean inheritGraphicState) {
        columnText.setInheritGraphicState(inheritGraphicState);
    }

    public float getRightIndent() {
        return columnText.getRightIndent();
    }

    public void setIndent(float indent, boolean repeatFirstLineIndent) {
        columnText.setIndent(indent, repeatFirstLineIndent);
    }

    public boolean isUseAscender() {
        return columnText.isUseAscender();
    }

    public void setAlignment(int alignment) {
        columnText.setAlignment(alignment);
    }

    public float getDescender() {
        return columnText.getDescender();
    }

    public ColumnTextBuilder setSimpleColumn(float llx, float lly, float urx, float ury, float leading,
            int alignment) {
        this.simpleColumnRectangle = new Rectangle(llx, lly, urx, ury);
        columnText.setSimpleColumn(llx, lly, urx, ury, leading, alignment);
        return this;
    }

    public void setFollowingIndent(float indent) {
        columnText.setFollowingIndent(indent);
    }

    public static float getWidth(Phrase phrase) {
        return ColumnText.getWidth(phrase);
    }

    public void updateFilledWidth(float w) {
        columnText.updateFilledWidth(w);
    }

    public void setRightIndent(float indent) {
        columnText.setRightIndent(indent);
    }

    public void setArabicOptions(int arabicOptions) {
        columnText.setArabicOptions(arabicOptions);
    }

    public void setRunDirection(int runDirection) {
        columnText.setRunDirection(runDirection);
    }

    public float getFollowingIndent() {
        return columnText.getFollowingIndent();
    }

    public void setLeading(float fixedLeading, float multipliedLeading) {
        columnText.setLeading(fixedLeading, multipliedLeading);
    }

    public ColumnTextBuilder setYLine(float yLine) {
        columnText.setYLine(yLine);
        return this;
    }

    public ColumnTextBuilder setSimpleColumn(Rectangle rect) {
        return setSimpleColumn(rect, false);
    }

    public ColumnTextBuilder setSimpleColumn(Rectangle rect, boolean saveYLine) {
        float yLine = 0;

        if (saveYLine) {
            yLine = getYLine();
        }

        Preconditions.checkArgument(!frozen, "can't change column when frozen");

        columnText.setSimpleColumn(rect);
        this.simpleColumnRectangle = rect;

        this.pageBottom = iTextBuilder.document.bottom();
        this.pageTop = iTextBuilder.document.top();

        if (saveYLine) {
            setYLine(yLine);
        }

        return this;
    }

    public static float getWidth(Phrase phrase, int runDirection, int arabicOptions) {
        return ColumnText.getWidth(phrase, runDirection, arabicOptions);
    }

    public void setSimpleColumn(Phrase phrase, float llx, float lly, float urx, float ury, float leading,
            int alignment) {
        columnText.setSimpleColumn(phrase, llx, lly, urx, ury, leading, alignment);
    }

    public void setUseAscender(boolean useAscender) {
        columnText.setUseAscender(useAscender);
    }

    public ColumnTextBuilder addText(Phrase phrase) {
        columnText.addText(phrase);
        return this;
    }

    public int getAlignment() {
        return columnText.getAlignment();
    }

    public List<Element> getCompositeElements() {
        return columnText.getCompositeElements();
    }

    public ColumnTextBuilder verticalSeparator(float h) {
        columnText.setYLine(columnText.getYLine() - h);

        return this;
    }

    public ColumnTextBuilder newLine() {
        columnText.addText(Chunk.NEWLINE);
        return this;
    }

    public void setPageBottom(float pageBottom) {
        this.pageBottom = pageBottom;
    }

    public void setPageTop(float pageTop) {
        this.pageTop = pageTop;
    }

    public ColumnTextBuilder dup() {
        final ColumnTextBuilder b = new ColumnTextBuilder(iTextBuilder);

        b.simpleColumnRectangle = new Rectangle(simpleColumnRectangle);
        b.columnText = ColumnText.duplicate(columnText);
        b.pageBottom = pageBottom;
        b.pageTop = pageTop;

        return b;
    }

    public RectangleBuilder getCurrentRectangle() {
        return iTextBuilder.newRectangleBuilder(new Rectangle(simpleColumnRectangle)).setTop(getYLine());
    }

    public ColumnTextBuilder addTruncatedLine(Chunk chunk, boolean addEllipsis) {
        final float pixelsForEllipsis = 6;

        try {
            ColumnText dup = ColumnText.duplicate(columnText);

            final Rectangle oneLineRectangle = new Rectangle(simpleColumnRectangle);

            oneLineRectangle.setTop(dup.getYLine());

            final float fontHeight = calcApproximateFontHeight(chunk.getFont()) * 1.6f;

            oneLineRectangle.setBottom(dup.getYLine() - fontHeight);

            if (addEllipsis) {
                oneLineRectangle.setRight(oneLineRectangle.getRight() - pixelsForEllipsis);
            }

            dup.setSimpleColumn(oneLineRectangle);
            dup.addText(chunk);

            final int status = dup.go();

            float yLine;

            if (addEllipsis && ColumnText.hasMoreText(status)) {
                oneLineRectangle.setLeft(dup.getLastX() + 2);
                oneLineRectangle.setRight(oneLineRectangle.getRight() + pixelsForEllipsis * 2);

                dup = ColumnText.duplicate(dup);

                dup.setSimpleColumn(oneLineRectangle);

                final Chunk ellipses = new Chunk("...\n", chunk.getFont());

                dup.setText(new Phrase(ellipses));
                dup.go();
                yLine = dup.getYLine();
            } else {
                yLine = dup.getYLine();
            }

            setYLine(yLine);

            return this;

        } catch (DocumentException e) {
            throw Exceptions.runtime(e);
        }
    }

    private static float calcApproximateFontHeight(Font font) {
        final BaseFont baseFont = font.getBaseFont();

        return baseFont.getFontDescriptor(BaseFont.ASCENT, font.getSize())
                - baseFont.getFontDescriptor(BaseFont.DESCENT, font.getSize());
    }

    public ColumnTextBuilder adjustBottom(float newRectHeight) {
        new RectangleBuilder().reuse(simpleColumnRectangle).adjustBottom(newRectHeight);

        setSimpleColumn(simpleColumnRectangle);

        return this;
    }

    public ColumnTextBuilder growBottom(float growBy) {
        iTextBuilder.reusableRectangleBuilder.reuse(simpleColumnRectangle).growBottom(growBy);

        float yLine = getYLine();

        setSimpleColumn(simpleColumnRectangle);

        setYLine(yLine);

        return this;
    }

    public double getCurrentHeight() {
        return simpleColumnRectangle.getTop() - getYLine();
    }

    public float getBottom() {
        return simpleColumnRectangle.getBottom();
    }

    public float getRight() {
        return simpleColumnRectangle.getRight();
    }

    public float getLeft() {
        return simpleColumnRectangle.getLeft();
    }

    public float getTop() {
        return simpleColumnRectangle.getTop();
    }

    public ColumnTextBuilder freeze() {
        frozen = true;
        return this;
    }

    public static enum GrowthResultType {
        PAGE_OVERFLOW, NORMAL, NO_MORE_CONTENT
    }

    public Iterator<AtomicIncreaseResult> newAtomicIteratorFor() {
        return newAtomicIteratorFor(null);
    }

    public Iterator<AtomicIncreaseResult> newAtomicIteratorFor(@Nullable Element element) {
        final Rectangle rect = getSimpleColumnRectangle();

        final RectangleBuilder original = iTextBuilder.newRectangleBuilder(rect);

        rect.setBottom(rect.getTop());

        setSimpleColumn(rect, true);

        return newAtomicIteratorFor(element, iTextBuilder.newRectangleBuilder(rect), original);
    }

    public Iterator<AtomicIncreaseResult> newAtomicIteratorFor(@Nullable Element element,
            final RectangleBuilder modifiableRectangle, RectangleBuilder originalRectangle) {
        if (element != null) {
            addElement(element);
        }

        final GrowStrategy growStrategy;

        if (element == null) {
            growStrategy = DEFAULT_GROW_STRATEGY;
        } else if (element instanceof Phrase) {
            Phrase phrase = (Phrase) element;
            final float approxHeight = phrase.getTotalLeading();

            growStrategy = new SimpleGrowStrategy(approxHeight);
        } else if (element instanceof com.itextpdf.text.List) {
            com.itextpdf.text.List list = (com.itextpdf.text.List) element;

            float lineHeight = 10f;

            final ListItem item = list.getFirstItem();

            if (item != null) {
                lineHeight = item.getTotalLeading();
            }

            growStrategy = new SimpleGrowStrategy(lineHeight);
        } else if (element instanceof PdfPTable) {
            PdfPTable table = (PdfPTable) element;

            float lineHeight = Float.MAX_VALUE;

            ArrayList<PdfPRow> rows = table.getRows();
            for (int i = 0; i < rows.size(); i++) {
                lineHeight = Math.min(lineHeight, table.getRowHeight(i));
            }

            if (lineHeight == Float.MAX_VALUE) {
                lineHeight = 10f;
            }

            growStrategy = new SimpleGrowStrategy(lineHeight);
        } else {
            throw new UnsupportedOperationException("todo: support element: " + element.getClass().getSimpleName());
        }

        return newAtomicIteratorFor(modifiableRectangle, growStrategy, originalRectangle);
    }

    public static interface GrowStrategy {
        void apply(int step, RectangleBuilder modifiableRect);
    }

    public static final class SimpleGrowStrategy implements GrowStrategy {
        final double approxHeight;
        float smallStep;
        float bigStep;

        public SimpleGrowStrategy(double approxMinLineHeight) {
            this.approxHeight = approxMinLineHeight;
            smallStep = (float) (approxMinLineHeight / 5);
            bigStep = (float) (approxMinLineHeight * 0.8);
        }

        @Override
        public final void apply(int step, RectangleBuilder modifiableRect) {
            if (step == 1) {
                modifiableRect.growBottom(bigStep);
            } else {
                modifiableRect.growBottom(smallStep);
            }
        }
    }

    public Iterator<AtomicIncreaseResult> newAtomicIteratorFor(final RectangleBuilder modifiableRectangle,
            final GrowStrategy growStrategy, RectangleBuilder originalRectangle) {
        final int status = go(true);

        if (!ColumnText.hasMoreText(status)) {
            return Iterators.emptyIterator();
        }

        return new AtomicIterator(modifiableRectangle, growStrategy, originalRectangle);
    }

    static final Field bidiLineField;
    static final Field compositeElementsField;
    static final Field totalTextLength;

    static {
        try {
            bidiLineField = ColumnText.class.getDeclaredField("bidiLine");
            bidiLineField.setAccessible(true);

            compositeElementsField = ColumnText.class.getDeclaredField("compositeElements");
            compositeElementsField.setAccessible(true);

            totalTextLength = BidiLine.class.getDeclaredField("totalTextLength");
            totalTextLength.setAccessible(true);
        } catch (NoSuchFieldException e) {
            ITextBuilder.logger.info(e.toString());
            throw Exceptions.runtime(e);
        }
    }

    /**
     * @deprecated fix: add version for composite mode
     */
    @Deprecated
    public int getVersion() {
        try {
            final Object bidiLine = bidiLineField.get(columnText);

            if (bidiLine == null) {
                return 0;
            }

            return (Integer) totalTextLength.get(bidiLine);
        } catch (IllegalAccessException e) {
            throw Exceptions.runtime(e);
        }
    }

    public Rectangle getSimpleColumnRectangle() {
        return simpleColumnRectangle;
    }

    public boolean fits(Element... element) {
        return fits(iTextBuilder.reuseColumnTextBuilder(), element);
    }

    public boolean fits(ColumnTextBuilder reusableCtb, Element... element) {
        reusableCtb.clearContent().setSimpleColumn(getCurrentRectangle().get());

        for (Element el : element) {
            if (el instanceof SpaceElement) {
                final SpaceElement space = (SpaceElement) el;
                if (space.fits(reusableCtb)) {
                    space.add(reusableCtb, true);
                } else {
                    return false;
                }
            } else {
                reusableCtb.addElement(el);

                if (ColumnText.hasMoreText(reusableCtb.go(true))) {
                    return false;
                }
            }
        }

        return true;
    }

    public ColumnTextBuilder saveState() {
        if (storedState == null) {
            storedState = iTextBuilder.newColumnTextBuilder();
        }
        storedState.setACopy(this);

        return this;
    }

    public ColumnTextBuilder restoreState() {
        setACopy(storedState);

        return this;
    }

    private class AtomicIterator extends AbstractIterator<AtomicIncreaseResult> {
        private final RectangleBuilder modifiableRectangle;
        private final GrowStrategy growStrategy;
        boolean finishOnNext;

        private final RectangleBuilder originalRectangle;

        public AtomicIterator(RectangleBuilder modifiableRectangle, GrowStrategy growStrategy,
                RectangleBuilder originalRectangle) {
            this.modifiableRectangle = modifiableRectangle;
            this.growStrategy = growStrategy;
            this.originalRectangle = originalRectangle;
        }

        public void restore() {
            setSimpleColumn(originalRectangle.get(), true);
        }

        @Override
        protected AtomicIncreaseResult computeNext() {
            if (finishOnNext) {
                endOfData();
                return null;
            }

            int stepCount = 0;

            final AtomicIncreaseResult r;

            int status = go(true);

            if (!ColumnText.hasMoreText(status)) {
                return new AtomicIncreaseResult(0, GrowthResultType.NORMAL);
            }

            float bottomBefore = modifiableRectangle.getBottom();

            while (true) {
                stepCount++;

                growStrategy.apply(stepCount, modifiableRectangle);

                if (modifiableRectangle.getBottom() < pageBottom) {
                    r = new AtomicIncreaseResult(bottomBefore - modifiableRectangle.getBottom(),
                            GrowthResultType.PAGE_OVERFLOW);
                    finishOnNext();
                    break;
                }

                setSimpleColumn(modifiableRectangle.get(), true);

                final float v1 = getYLine();

                // iText has a bug here: it can't handle keepTogether()
                // because it presumes that items would never be rolled back
                // so it adds line by line, adding line heights to yLine.
                // So on the iteration, when I have 16px rect, one line fits,
                // I increase the height by 3px, it false-positively adds the second line
                // which fits the 16+3-height rectangle.
                // So force it to rollback to the previous state by invoking restoreState(),
                // so that it would add items from scratch.
                saveState();

                status = go(true);

                if (!ColumnText.hasMoreText(status)) {
                    r = new AtomicIncreaseResult(bottomBefore - modifiableRectangle.getBottom(),
                            GrowthResultType.NO_MORE_CONTENT);
                    finishOnNext();
                    break;
                }

                final float v2 = getYLine();

                if (v1 != v2) {
                    r = new AtomicIncreaseResult(bottomBefore - modifiableRectangle.getBottom(),
                            GrowthResultType.NORMAL);
                    break;
                }

                restoreState();
            }

            return r;
        }

        private void finishOnNext() {
            finishOnNext = true;
        }
    }

    public ColumnTextBuilder flipPageForSpace(float height) {
        if (getYLine() - height < iTextBuilder.document.bottom()) {
            iTextBuilder.document.newPage();
            onPageFlip();
        }

        return this;
    }

    public void onPageFlip() {
        final RectangleBuilder rect = getCurrentRectangle().setTop(iTextBuilder.document.top())
                .setBottom(iTextBuilder.document.bottom());

        setSimpleColumn(rect.get(), false);
    }
}