/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 org.apache.camel.component.pdf.text; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; import org.apache.camel.component.pdf.PdfConfiguration; import org.apache.commons.lang3.builder.ToStringBuilder; import static org.apache.camel.component.pdf.PdfConstants.MIN_CONTENT_WIDTH; /** * Builds lines from words based on line width and PDF document page size. * Built lines then will be written to pdf document. */ public class DefaultLineBuilderStrategy implements LineBuilderStrategy { private final PdfConfiguration pdfConfiguration; public DefaultLineBuilderStrategy(PdfConfiguration pdfConfiguration) { this.pdfConfiguration = pdfConfiguration; } /** * Builds lines from words. Utilizes the same behaviour as office software: * <ul> * <li>If word doesn't fit in current line, and current lines contains other words, then * it will be moved to new line.</td> * <li>Word doesn't fit in the line and line does not contain other words, then word will be * slitted, and split index will be on max amount of characters that fits in the line </li> * </ul> */ @Override public Collection<String> buildLines(Collection<String> splittedText) throws IOException { LinkedList<String> wordsList = new LinkedList<String>(splittedText); Collection<String> lines = new ArrayList<String>(); LineBuilder currentLine = new LineBuilder(); float allowedLineWidth = getAllowedLineWidth(); while (!wordsList.isEmpty()) { String word = wordsList.removeFirst(); if (isWordFitInCurrentLine(currentLine, word, allowedLineWidth)) { currentLine.appendWord(word); if (wordsList.isEmpty()) { lines.add(currentLine.buildLine()); } } else if (currentLine.getWordsCount() != 0) { lines.add(currentLine.buildLine()); wordsList.addFirst(word); } else { int splitIndex = findSplitIndex(word, allowedLineWidth); currentLine.appendWord(word.substring(0, splitIndex)); lines.add(currentLine.buildLine()); wordsList.addFirst(word.substring(splitIndex)); } } return lines; } private int findSplitIndex(String word, float allowedLineWidth) throws IOException { // Using binary search algorithm to find max amount of characters that fit int the line. int middle = word.length() >> 1; int end = word.length(); int currentSplitIndex = 0; do { if (isLineFitInLineWidth(word.substring(0, middle), allowedLineWidth)) { currentSplitIndex = middle; middle += word.substring(middle, end).length() >> 1; } else { end = middle; middle = currentSplitIndex + (word.substring(currentSplitIndex, middle).length() >> 1); } } while ((currentSplitIndex == -1) || !isSplitIndexFound(word, allowedLineWidth, currentSplitIndex)); return currentSplitIndex; } private boolean isSplitIndexFound(String word, float allowedLineWidth, int currentSplitIndex) throws IOException { return isLineFitInLineWidth(word.substring(0, currentSplitIndex), allowedLineWidth) && !isLineFitInLineWidth(word.substring(0, currentSplitIndex + 1), allowedLineWidth); } private boolean isWordFitInCurrentLine(LineBuilder currentLine, String word, float allowedLineWidth) throws IOException { LineBuilder lineBuilder = currentLine.clone().appendWord(word); return isLineFitInLineWidth(lineBuilder.buildLine(), allowedLineWidth); } private boolean isLineFitInLineWidth(String currentLine, float allowedLineWidth) throws IOException { float fontWidth = PdfUtils.getFontWidth( currentLine, pdfConfiguration.getFont(), pdfConfiguration.getFontSize()); return fontWidth <= allowedLineWidth; } public float getAllowedLineWidth() { float result = pdfConfiguration.getPageSize().getWidth() - pdfConfiguration.getMarginLeft() - pdfConfiguration.getMarginRight(); if (result < MIN_CONTENT_WIDTH) { throw new IllegalStateException(String.format("Allowed line width cannot be < %d, make sure" + " (marginLeft + marginRight) < pageSize", MIN_CONTENT_WIDTH)); } return result; } private static final class LineBuilder { private StringBuilder line = new StringBuilder(); private int wordsCount; LineBuilder() { } LineBuilder(String line, int wordsCount) { this.line = new StringBuilder(line); this.wordsCount = wordsCount; } public LineBuilder appendWord(String word) { line.append(word).append(" "); wordsCount++; return this; } public String buildLine() { String line = this.line.toString(); reset(); return line; } public int getWordsCount() { return wordsCount; } private void reset() { line = new StringBuilder(); wordsCount = 0; } public LineBuilder clone() { return new LineBuilder(this.line.toString(), this.wordsCount); } @Override public String toString() { return ToStringBuilder.reflectionToString(this); } } }