Java tutorial
/* * 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.columns; import com.chaschev.itext.*; import com.google.common.base.Preconditions; import com.itextpdf.text.BaseColor; import com.itextpdf.text.Element; import com.itextpdf.text.Rectangle; import com.itextpdf.text.pdf.ColumnText; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Iterator; import java.util.List; /** * todo: test page breaks * todo: table support * todo: list support * todo: test spaces */ /** * User: chaschev * Date: 9/8/13 */ public class BalancedColumnsBuilder { public static final Logger logger = LoggerFactory.getLogger(BalancedColumnsBuilder.class); private static final double MINIMAL_HEIGHT_COEFFICIENT = 0.70; private int startAtElement = 0; private ColumnTextBuilder initialLeftCTB; private ElementSequence sequence = new ElementSequence(); RectangleBuilder origRectangle; ITextBuilder b; private static final int MAX_PAGE_COUNT = 20; float hPadding = 5; private RectangleBuilder singleColumnRect = new RectangleBuilder().reuse(new Rectangle(0, 0, 0, 0)); private float additionalSpaceForColumn = 10f; private transient double minimalLeftColumnHeight; private double referenceHeight; private final SplitResult currentLeftResult = new SplitResult(); private final SplitResult currentRightResult = new SplitResult(); private ColumnTextBuilder leftCTB; private ColumnTextBuilder rightCTB; final SplitResult bestResult = new SplitResult().worstResult(); private @Nullable ColumnTextBuilder updateAfterRun; public BalancedColumnsBuilder() { } public BalancedColumnsBuilder(Rectangle rectangle, ITextBuilder b) { this.origRectangle = new RectangleBuilder().reuse(rectangle); this.b = b; } public BalancedColumnsBuilder(ColumnTextBuilder ctb, ITextBuilder b) { setColumnText(ctb, b); } public BalancedColumnsBuilder setColumnText(ColumnTextBuilder ctb, ITextBuilder b) { this.origRectangle = new RectangleBuilder().reuse(ctb.getCurrentRectangle().get()); updateAfterRun = ctb; this.b = b; return this; } public BalancedColumnsBuilder(ColumnTextBuilder initialLeftCTB, int startAtElement, Rectangle rectangle, ITextBuilder b) { this.initialLeftCTB = initialLeftCTB.freeze(); this.startAtElement = startAtElement; this.origRectangle = new RectangleBuilder().reuse(rectangle); this.b = b; } public BalancedColumnsBuilder add(Element... element) { for (Element el : element) { sequence.add(el); } return this; } public double heightPenalty(double columnHeight) { double v = columnHeight - referenceHeight; if (v < 0) return v = 0; return v * 1.0; } public BalancingResult go() { Preconditions.checkNotNull(origRectangle); Preconditions.checkNotNull(b); return _go(); } public static class BalancingResult { public float yLine; public BalancingResult(float yLine) { this.yLine = yLine; } } /** * */ private class DirectContentAdder { ColumnTextBuilder startWith; @Nonnull final ColumnTextBuilder dest; int startAtIndex; boolean simulate = true; double quickHeight; @Nullable private ColumnTextBuilder rightCTB; private boolean setHeights; private float startContentHeight = -10000; private DirectContentAdder(@Nonnull ColumnTextBuilder dest) { this.dest = dest; } public DirectContentAdder setStartWith(ColumnTextBuilder startWith) { this.startWith = startWith; return this; } public DirectContentAdder setStartAtIndex(int startAtIndex) { this.startAtIndex = startAtIndex; return this; } public DirectContentAdder setSwitchToRightCTB(ColumnTextBuilder rightCTB) { this.rightCTB = rightCTB; return this; } public DirectContentAdder setSimulate(boolean simulate) { this.simulate = simulate; return this; } public DirectContentAdder setQuickHeight(double quickHeight) { this.quickHeight = quickHeight; return this; } public DirectContentAdder setHeights(boolean setHeights) { this.setHeights = setHeights; return this; } public boolean isHeights() { return setHeights; } public class Result { int status; int index; ColumnTextBuilder contentLeft; public Result(int status, int index, ColumnTextBuilder contentLeft) { this.status = status; this.index = index; this.contentLeft = contentLeft; } public boolean hasContentLeft(int totalElementCount) { return !(contentLeft == null && totalElementCount == index); } } public Result go() { if (quickHeight != 0) { if (rightCTB != null) { throw new IllegalStateException("if quickHeight !=0 then rightCTB == null!"); } } Preconditions.checkNotNull(dest); ColumnTextBuilder currentCtb = dest; List<Element> elements = sequence.getElements(); int i; if (quickHeight != 0) { currentCtb.adjustBottom((float) quickHeight); } if (startWith != null) { currentCtb.copyContentFrom(startWith); float yBefore = currentCtb.getYLine(); int status = currentCtb.go(simulate); if (setHeights) { startContentHeight = yBefore - currentCtb.getYLine(); } if (ColumnText.hasMoreText(status)) { if (rightCTB != null) { // => quickHeight == 0 rightCTB.copyContentFrom(currentCtb); currentCtb = rightCTB; yBefore = currentCtb.getYLine(); status = currentCtb.go(simulate); if (setHeights) { startContentHeight += yBefore - currentCtb.getYLine(); } if (ColumnText.hasMoreText(status)) { return new Result(ColumnText.NO_MORE_COLUMN, startAtIndex, currentCtb); } } else { return new Result(ColumnText.NO_MORE_COLUMN, startAtIndex, currentCtb); } } } //optimisation mode if (quickHeight != 0) { for (i = startAtIndex; i < elements.size(); i++) { Element el = elements.get(i); if (currentCtb.fits(el)) { if (el instanceof SpaceElement) { SpaceElement spaceElement = (SpaceElement) el; spaceElement.add(currentCtb, simulate); currentCtb.go(simulate); } else { currentCtb.addElement(el).go(simulate); } } else { break; } } return new Result(ColumnText.NO_MORE_TEXT, i, null); } for (i = startAtElement; i < elements.size(); i++) { Element el = elements.get(i); float yBefore = currentCtb.getYLine(); if (el instanceof SpaceElement) { SpaceElement space = (SpaceElement) el; if (space.fits(currentCtb, currentCtb.getBottom())) { space.add(currentCtb, simulate); } else { if (currentCtb == dest) { currentCtb = rightCTB; } else if (currentCtb == rightCTB) { return new Result(ColumnText.NO_MORE_COLUMN, i + 1, currentCtb.clearContent()); } } } else { currentCtb.addElement(el); if (ColumnText.hasMoreText(currentCtb.go(simulate))) { if (currentCtb == dest) { if (rightCTB == null) { return new Result(ColumnText.NO_MORE_COLUMN, i + 1, currentCtb); } else { rightCTB.copyContentFrom(currentCtb); currentCtb = rightCTB; final int status = currentCtb.go(simulate); if (ColumnText.hasMoreText(status)) { return new Result(ColumnText.NO_MORE_COLUMN, i + 1, currentCtb); } } } else { return new Result(ColumnText.NO_MORE_COLUMN, i + 1, currentCtb); } } } if (setHeights) { sequence.setHeight(i, yBefore - currentCtb.getYLine()); } } return new Result(ColumnText.NO_MORE_TEXT, elements.size(), null); } } private BalancingResult _go() { if (b.drawBorders) { b.getCanvasBuilder().drawGrayRectangle(origRectangle.get(), BaseColor.LIGHT_GRAY); } currentLeftResult.totalElementCount = currentRightResult.totalElementCount = bestResult.totalElementCount = sequence .size(); //try adding into a single infinite column to calc height final float hCenter = horCenter(); referenceHeight = calcReferenceHeight(hCenter); currentLeftResult.referenceHeight = currentRightResult.referenceHeight = bestResult.referenceHeight = referenceHeight; leftCTB = setColumn((float) referenceHeight, hCenter, true, true, singleColumnRect, b.newColumnTextBuilder()); rightCTB = setColumn((float) referenceHeight, hCenter, false, true, singleColumnRect, b.newColumnTextBuilder()); minimalLeftColumnHeight = MINIMAL_HEIGHT_COEFFICIENT * referenceHeight / 2; int i; List<Element> elements = sequence.getElements(); final DirectContentAdder.Result quickResult = new DirectContentAdder(leftCTB).setStartWith(initialLeftCTB) .setStartAtIndex(startAtElement).setQuickHeight(minimalLeftColumnHeight).setSimulate(true).go(); i = quickResult.index; bestResult.assignIfWorseThan(currentLeftResult.setElementsAddedCount(i).setLeftElementSplitHeight(0, 0) .setLeftColumnHeight(leftCTB.getCurrentHeight()) .setPageSplit(i, quickResult.hasContentLeft(elements.size()))); boolean pageOverFlow = false; //the only situation possible is the content left from initialContent if (quickResult.contentLeft != null) { leftCTB.copyContentFrom(quickResult.contentLeft); pageOverFlow = iterateOnLeft(leftCTB.newAtomicIteratorFor(), i - 1, currentLeftResult, leftCTB.getTop(), sequence.initialContentHeight); } if (i == elements.size()) { bestResult.assignIfWorseThan(currentLeftResult.setLeftElementSplitHeight(0, 0) .setLeftColumnHeight(leftCTB.getCurrentHeight()).setElementsAddedCount(i) .setPageSplit(i, leftCTB.hasMoreText())); } if (pageOverFlow) { return applyBestResult(); } elementsCycle: for (; i < elements.size(); i++) { Element el = elements.get(i); final SplitResult currentResult = currentLeftResult; currentResult.setLeftElementSplitHeight(0, 0).setElementsAddedCount(i) .setLeftColumnHeight(leftCTB.getCurrentHeight()); considerAddingOnRightAtWholeElement(i); if (el instanceof SpaceElement) { SpaceElement space = (SpaceElement) el; currentResult.setElementsAddedCount(i); considerAddingToRight(i + 1, false); if (space.fits(leftCTB, origRectangle.getBottom())) { space.add(leftCTB, true); } else { if (leftCTB.getSimpleColumnRectangle().getBottom() - space.getHeight() < b.getDocument() .bottom()) { break; } leftCTB.growBottom(space.getHeight()).setYLine(leftCTB.getYLine() - space.getHeight()); } } else { float elementTop = leftCTB.getYLine(); final Iterator<AtomicIncreaseResult> iterator = leftCTB.newAtomicIteratorFor(el); if (iterateOnLeft(iterator, i, currentResult, elementTop, sequence.getHeight(i))) { break elementsCycle; } // leftCTB.restoreState(); } currentResult.setLeftColumnHeight(leftCTB.getCurrentHeight()).setElementsAddedCount(i + 1); } considerAddingOnRightAtWholeElement(i); //here we have bestResult, so let's add our content with no simulation! // setColumn(bestResult, b.newColumnTextBuilder()) return applyBestResult(); } private void considerAddingOnRightAtWholeElement(int i) { if (i > 0 && !previousIsSpace(i)) { considerAddingToRight(i, false); } } private BalancingResult applyBestResult() { final float hCenter = horCenter(); List<Element> elements = sequence.getElements(); setColumn((float) bestResult.leftColumnHeight, hCenter, true, true, singleColumnRect, leftCTB); setColumn((float) bestResult.rightColumnHeight, hCenter, false, false, singleColumnRect, rightCTB); if (b.drawBorders) { b.getCanvasBuilder().drawGrayRectangle(leftCTB.getSimpleColumnRectangle(), BaseColor.RED); b.getCanvasBuilder().drawGrayRectangle(rightCTB.getSimpleColumnRectangle(), BaseColor.GREEN); } leftCTB.clearContent(); rightCTB.clearContent(); final DirectContentAdder.Result addResult = new DirectContentAdder(leftCTB).setStartWith(initialLeftCTB) .setStartAtIndex(startAtElement).setSwitchToRightCTB(rightCTB).setSimulate(false).go(); float yLine = Math.min(leftCTB.getYLine(), rightCTB.getYLine()); final BalancingResult r; if (addResult.hasContentLeft(elements.size())) { final ColumnTextBuilder contentCopy = addResult.contentLeft == null ? null : b.newColumnTextBuilder().setACopy(addResult.contentLeft); r = startWithANewPage(contentCopy, addResult.index); } else { r = new BalancingResult(yLine); // if(updateAfterRun != null){ // updateAfterRun.growBottom(origRectangle.getTop() - yLine); // } } if (updateAfterRun != null) { updateAfterRun.setSimpleColumn(b.reuseRectangleBuilder(new Rectangle(origRectangle.get())).setTop(yLine) .setBottom(b.getDocument().bottom()).get()); } return r; } private boolean iterateOnLeft(Iterator<AtomicIncreaseResult> iterator, int currentIndex, SplitResult currentResult, float elementTop, float elementHeight) { currentResult.setLeftColumnHeight(leftCTB.getCurrentHeight()) .setLeftElementSplitHeight(elementTop - leftCTB.getYLine(), elementHeight); if (currentResult.leftElementSplitHeight > 0) { considerAddingToRight(currentIndex + 1, true); } while (iterator.hasNext()) { AtomicIncreaseResult r = iterator.next(); currentResult.setLeftColumnHeight(leftCTB.getCurrentHeight()) .setLeftElementSplitHeight(elementTop - leftCTB.getYLine(), elementHeight); switch (r.type) { case PAGE_OVERFLOW: return true; case NORMAL: considerAddingToRight(currentIndex + 1, true); break; case NO_MORE_CONTENT: //don't consider adding to right as this case must be checked in the main loop // considerAddingToRight(currentIndex + 1, false); break; } } return false; } private boolean previousIsSpace(int currentIndex) { return sequence.size() > 0 && currentIndex > 0 && (sequence.getElements().get(currentIndex - 1) instanceof SpaceElement); } private BalancingResult startWithANewPage(ColumnTextBuilder content, int startAt) { //noinspection SimplifiableConditionalExpression logger.debug("starting with a new page, content: {}, startAt: {}", content == null ? false : content.hasMoreText(), startAt); final RectangleBuilder rect = b.reuseRectangleBuilder(origRectangle.get()).setTop(b.getDocument().top()) .setBottom(b.getDocument().bottom()); b.getDocument().newPage(); return new BalancedColumnsBuilder(content, startAt, rect.get(), b).setSequence(sequence).go(); } private float calcReferenceHeight(float hCenter) { float yBefore = origRectangle.getTop(); ColumnTextBuilder tempCtb = b.newColumnTextBuilder(); singleColumnRect.copyPositionsFrom(origRectangle); singleColumnRect.setBottom(-100000f); singleColumnRect.setRight(hCenter); applyPadding(singleColumnRect, true); tempCtb.setSimpleColumn(singleColumnRect.get()); final DirectContentAdder adder = new DirectContentAdder(tempCtb); adder.setStartWith(initialLeftCTB).setStartAtIndex(startAtElement).setSimulate(true).setHeights(true).go(); sequence.initialContentHeight = adder.startContentHeight; float yAfter = tempCtb.getYLine(); return (yBefore - yAfter); } private void considerAddingToRight(int startAt, boolean hasNotFlushedText) { logger.trace("adding to right, i: {}, leftHeight: {}, leftSplit: {}", startAt, currentLeftResult.leftColumnHeight, currentLeftResult.leftElementSplitHeight); //copy content from the left rightCTB.setACopy(leftCTB).setYLine(leftCTB.getTop()); if (!hasNotFlushedText && sequence.isSpace(startAt)) { startAt++; } //we can flush what's left from the previous column if (rightCTB.hasMoreText()) { rightCTB.go(true); } currentRightResult.copyFrom(currentLeftResult); //copied from left method int i; List<Element> elements = sequence.getElements(); final DirectContentAdder.Result quickResult = new DirectContentAdder(rightCTB).setStartWith(leftCTB) .setStartAtIndex(startAt).setQuickHeight(leftCTB.getCurrentHeight()).setSimulate(true).go(); i = quickResult.index; int elementsAdded = quickResult.contentLeft == null ? i : i - 1; bestResult.assignIfWorseThan(currentRightResult.setElementsAddedCount(elementsAdded) //todo!!! .setRightElementSplitHeight(0, 0).setRightColumnHeight(rightCTB.getCurrentHeight()) .setPageSplit(elementsAdded, quickResult.hasContentLeft(elements.size()))); boolean pageOverFlow = false; if (quickResult.contentLeft != null) { rightCTB.copyContentFrom(quickResult.contentLeft); AtomicIncreaseResult lastResult = iterateOnRight(i - 1, currentRightResult, rightCTB.getTop(), rightCTB.newAtomicIteratorFor()); if (lastResult.type == ColumnTextBuilder.GrowthResultType.PAGE_OVERFLOW) { pageOverFlow = true; } } if (i == elements.size()) { bestResult.assignIfWorseThan(currentRightResult.setRightElementSplitHeight(0, 0) .setRightColumnHeight(rightCTB.getCurrentHeight()).setElementsAddedCount(i) .setPageSplit(i, rightCTB.hasMoreText())); } if (pageOverFlow) return; elementsCycle: for (; i < elements.size(); i++) { Element el = elements.get(i); final SplitResult currentResult = currentRightResult; setFullAddedElementsStateRight(currentResult, i, rightCTB.getCurrentHeight()); currentResult.setPageSplit(true); //temporary pessimism if (el instanceof SpaceElement) { //todo extract method SpaceElement space = (SpaceElement) el; bestResult.assignIfWorseThan(currentResult.setElementsAddedCount(i).setPageSplit(i, false)); if (space.fits(rightCTB, origRectangle.getBottom())) { space.add(rightCTB, true); } else { if (rightCTB.getSimpleColumnRectangle().getBottom() - space.getHeight() < b.getDocument() .bottom()) { break; } rightCTB.growBottom(space.getHeight()).setYLine(rightCTB.getYLine() - space.getHeight()); } } else { float elementTop = rightCTB.getYLine(); final Iterator<AtomicIncreaseResult> iterator = rightCTB.newAtomicIteratorFor(el); AtomicIncreaseResult lastResult = iterateOnRight(i, currentResult, elementTop, iterator); if (lastResult.type == ColumnTextBuilder.GrowthResultType.PAGE_OVERFLOW) { break; } } //element is fully added here currentResult.setRightColumnHeight(rightCTB.getCurrentHeight()).setRightElementSplitHeight(0, 0); bestResult.assignIfWorseThan(currentResult); } } private AtomicIncreaseResult iterateOnRight(int currentIndex, SplitResult currentResult, float elementTop, Iterator<AtomicIncreaseResult> iterator) { AtomicIncreaseResult lastIncreaseResult = null; currentResult.setRightColumnHeight(rightCTB.getCurrentHeight()) .setRightElementSplitHeight(elementTop - rightCTB.getYLine(), sequence.getHeight(currentIndex)); bestResult.assignIfWorseThan(currentResult); cycle: while (iterator.hasNext()) { AtomicIncreaseResult r = iterator.next(); currentResult.setRightColumnHeight(rightCTB.getCurrentHeight()); lastIncreaseResult = r; switch (r.type) { case PAGE_OVERFLOW: break cycle; case NORMAL: currentResult.setRightElementSplitHeight(elementTop - rightCTB.getYLine(), sequence.getHeight(currentIndex)); bestResult.assignIfWorseThan(currentResult); break; case NO_MORE_CONTENT: //must be done in the main cycle // setFullAddedElementsStateRight(currentResult, currentIndex + 1, rightCTB.getCurrentHeight()); break cycle; } } return lastIncreaseResult == null ? new AtomicIncreaseResult(0, ColumnTextBuilder.GrowthResultType.NO_MORE_CONTENT) : lastIncreaseResult; } private void setFullAddedElementsStateRight(SplitResult result, int elementCount, double height) { result.setRightElementSplitHeight(0, 0).setRightColumnHeight(height).setElementsAddedCount(elementCount) .setPageSplit(elementCount, false); bestResult.assignIfWorseThan(result); } private ColumnTextBuilder setColumn(float height, float horCenter, boolean isLeft, boolean simulate, RectangleBuilder singleColumnRect, ColumnTextBuilder ctb) { // new RectangleBuilder(). final RectangleBuilder rect = b.newCopyRectangleBuilder(origRectangle.get()) .setBottom(singleColumnRect.getTop() - height); if (isLeft) { rect.setRight(horCenter); } else { rect.setLeft(horCenter); } applyPadding(rect, isLeft); logger.debug("setting column to: {}", rect); ctb.setSimpleColumn(rect.get()); return ctb; } private void applyPadding(RectangleBuilder r, boolean isLeft) { if (isLeft) { r.setRight(r.getRight() - hPadding); } else { r.setLeft(r.getLeft() + hPadding); } } private float horCenter() { return (origRectangle.getLeft() + origRectangle.getRight()) / 2; } private BalancedColumnsBuilder setSequence(ElementSequence sequence) { this.sequence = sequence; return this; } public BalancedColumnsBuilder updateAfterRun(ColumnTextBuilder ctb) { this.updateAfterRun = ctb; return this; } public BalancedColumnsBuilder setHPadding(float hPadding) { this.hPadding = hPadding; return this; } public BalancedColumnsBuilder trimSpaceElements() { sequence.trim(); return this; } public ElementSequence getSequence() { return sequence; } }