All Downloads are FREE. Search and download functionalities are using the official Maven repository.

be.quodlibet.boxable.Paragraph Maven / Gradle / Ivy

There is a newer version: 1.7.0
Show newest version

/*
 Quodlibet.be
 */
package be.quodlibet.boxable;

import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import be.quodlibet.boxable.utils.PageContentStreamOptimized;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;

import be.quodlibet.boxable.text.PipelineLayer;
import be.quodlibet.boxable.text.Token;
import be.quodlibet.boxable.text.TokenType;
import be.quodlibet.boxable.text.Tokenizer;
import be.quodlibet.boxable.text.WrappingFunction;
import be.quodlibet.boxable.utils.FontUtils;
import be.quodlibet.boxable.utils.PDStreamUtils;

public class Paragraph {

	private float width;
	private final String text;
	private float fontSize;
	private PDFont font;
	private final PDFont fontBold;
	private final PDFont fontItalic;
	private final PDFont fontBoldItalic;
	private final WrappingFunction wrappingFunction;
	private HorizontalAlignment align;
	private TextType textType;
	private Color color;
	private float lineSpacing;

	private final static int DEFAULT_TAB = 4;
	private final static int DEFAULT_TAB_AND_BULLET = 6;
	private final static int BULLET_SPACE = 2;

	private boolean drawDebug;
	private final Map lineWidths = new HashMap<>();
	private Map> mapLineTokens = new LinkedHashMap<>();
	private float maxLineWidth = Integer.MIN_VALUE;
	private List tokens;
	private List lines;
	private Float spaceWidth;

	public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align) {
		this(text, font, fontSize, width, align, null);
	}

	// This function exists only to preserve backwards compatibility for
	// the getWrappingFunction() method; it has been replaced with a faster implementation in the Tokenizer
	private static final WrappingFunction DEFAULT_WRAP_FUNC = new WrappingFunction() {
		@Override
		public String[] getLines(String t) {
			return t.split("(?<=\\s|-|@|,|\\.|:|;)");
		}
	};

	public Paragraph(String text, PDFont font, int fontSize, int width) {
		this(text, font, fontSize, width, HorizontalAlignment.LEFT, null);
	}

	public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
			WrappingFunction wrappingFunction) {
		this(text, font, fontSize, width, align, Color.BLACK, (TextType) null, wrappingFunction);
	}

	public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
			final Color color, final TextType textType, WrappingFunction wrappingFunction) {
		this(text, font, fontSize, width, align, color, textType, wrappingFunction, 1);
	}

	public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
			final Color color, final TextType textType, WrappingFunction wrappingFunction, float lineSpacing) {
		this.color = color;
		this.text = text;
		this.font = font;
		// check if we have different default font for italic and bold text
		if (FontUtils.getDefaultfonts().isEmpty()) {
			fontBold = PDType1Font.HELVETICA_BOLD;
			fontItalic = PDType1Font.HELVETICA_OBLIQUE;
			fontBoldItalic = PDType1Font.HELVETICA_BOLD_OBLIQUE;
		} else {
			fontBold = FontUtils.getDefaultfonts().get("fontBold");
			fontBoldItalic = FontUtils.getDefaultfonts().get("fontBoldItalic");
			fontItalic = FontUtils.getDefaultfonts().get("fontItalic");
		}
		this.fontSize = fontSize;
		this.width = width;
		this.textType = textType;
		this.setAlign(align);
		this.wrappingFunction = wrappingFunction;
		this.lineSpacing = lineSpacing;
	}

	public List getLines() {
		// memoize this function because it is very expensive
		if (lines != null) {
			return lines;
		}

		final List result = new ArrayList<>();

		// text and wrappingFunction are immutable, so we only ever need to compute tokens once
		if (tokens == null) {
			tokens = Tokenizer.tokenize(text, wrappingFunction);
		}

		int lineCounter = 0;
		boolean italic = false;
		boolean bold = false;
		boolean listElement = false;
		PDFont currentFont = font;
		int orderListElement = 1;
		int numberOfOrderedLists = 0;
		int listLevel = 0;
		Stack stack= new Stack<>();

		final PipelineLayer textInLine = new PipelineLayer();
		final PipelineLayer sinceLastWrapPoint = new PipelineLayer();

		for (final Token token : tokens) {
			switch (token.getType()) {
			case OPEN_TAG:
				if (isBold(token)) {
					bold = true;
					currentFont = getFont(bold, italic);
				} else if (isItalic(token)) {
					italic = true;
					currentFont = getFont(bold, italic);
				} else if (isList(token)) {
					listLevel++;
					if (token.getData().equals("ol")) {
						numberOfOrderedLists++;
						if(listLevel > 1){
							stack.add(new HTMLListNode(orderListElement-1, stack.isEmpty() ? String.valueOf(orderListElement-1)+"." : stack.peek().getValue() + String.valueOf(orderListElement-1) + "."));
						}
						orderListElement = 1;

						textInLine.push(sinceLastWrapPoint);
						// check if you have some text before this list, if you don't then you really don't need extra line break for that
						if (textInLine.trimmedWidth() > 0) {
							// this is our line
							result.add(textInLine.trimmedText());
							lineWidths.put(lineCounter, textInLine.trimmedWidth());
							mapLineTokens.put(lineCounter, textInLine.tokens());
							maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
							textInLine.reset();
							lineCounter++;
						}
					} else if (token.getData().equals("ul")) {
						textInLine.push(sinceLastWrapPoint);
						// check if you have some text before this list, if you don't then you really don't need extra line break for that
						if (textInLine.trimmedWidth() > 0) {
							// this is our line
							result.add(textInLine.trimmedText());
							lineWidths.put(lineCounter, textInLine.trimmedWidth());
							mapLineTokens.put(lineCounter, textInLine.tokens());
							maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
							textInLine.reset();
							lineCounter++;
						}
					}
				}
				sinceLastWrapPoint.push(token);
				break;
			case CLOSE_TAG:
				if (isBold(token)) {
					bold = false;
					currentFont = getFont(bold, italic);
					sinceLastWrapPoint.push(token);
				} else if (isItalic(token)) {
					italic = false;
					currentFont = getFont(bold, italic);
					sinceLastWrapPoint.push(token);
				} else if (isList(token)) {
					listLevel--;
					if (token.getData().equals("ol")) {
						numberOfOrderedLists--;
						// reset elements
						if(numberOfOrderedLists>0){
							orderListElement = stack.peek().getOrderingNumber()+1;
							stack.pop();
						}
					}
					// ensure extra space after each lists
					// no need to worry about current line text because last closing 
  • tag already done that if(listLevel == 0){ result.add(" "); lineWidths.put(lineCounter, 0.0f); mapLineTokens.put(lineCounter, new ArrayList()); lineCounter++; } } else if (isListElement(token)) { // wrap at last wrap point? if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) { // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; // wrapping at last wrap point if (numberOfOrderedLists>0) { String orderingNumber = stack.isEmpty() ? String.valueOf(orderListElement) + "." : stack.pop().getValue() + "."; stack.add(new HTMLListNode(orderListElement, orderingNumber)); try { float tab = indentLevel(DEFAULT_TAB); float orderingNumberAndTab = font.getStringWidth(orderingNumber) + tab; textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String .valueOf(orderingNumberAndTab / 1000 * getFontSize()))); } catch (IOException e) { e.printStackTrace(); } orderListElement++; } else { try { // if it's not left aligned then ignore list and list element and deal with it as normal text where
  • mimic
    behaviour float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tabBullet / 1000 * getFontSize()))); } catch (IOException e) { e.printStackTrace(); } } textInLine.push(sinceLastWrapPoint); } // wrapping at this must-have wrap point textInLine.push(sinceLastWrapPoint); // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; listElement = false; } if (isParagraph(token)) { if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) { // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); lineCounter++; textInLine.reset(); } // wrapping at this must-have wrap point textInLine.push(sinceLastWrapPoint); // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; // extra spacing because it's a paragraph result.add(" "); lineWidths.put(lineCounter, 0.0f); mapLineTokens.put(lineCounter, new ArrayList()); lineCounter++; } break; case POSSIBLE_WRAP_POINT: if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) { // this is our line if (!textInLine.isEmpty()) { result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); lineCounter++; textInLine.reset(); } // wrapping at last wrap point if (listElement) { if (numberOfOrderedLists>0) { try { float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB) : indentLevel(DEFAULT_TAB); String orderingNumber = stack.isEmpty() ? String.valueOf(orderListElement) + "." : stack.peek().getValue() + "." + String.valueOf(orderListElement-1) + "."; textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf((tab + font.getStringWidth(orderingNumber)) / 1000 * getFontSize()))); } catch (IOException e) { e.printStackTrace(); } } else { try { // if it's not left aligned then ignore list and list element and deal with it as normal text where
  • mimic
    behavior float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tabBullet / 1000 * getFontSize()))); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } textInLine.push(sinceLastWrapPoint); } else { textInLine.push(sinceLastWrapPoint); } break; case WRAP_POINT: // wrap at last wrap point? if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) { // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; // wrapping at last wrap point if (listElement) { if(!getAlign().equals(HorizontalAlignment.LEFT)) { listLevel = 0; } if (numberOfOrderedLists>0) { // String orderingNumber = String.valueOf(orderListElement) + ". "; String orderingNumber = stack.isEmpty() ? String.valueOf("1") + "." : stack.pop().getValue() + ". "; try { float tab = indentLevel(DEFAULT_TAB); float orderingNumberAndTab = font.getStringWidth(orderingNumber) + tab; textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String .valueOf(orderingNumberAndTab / 1000 * getFontSize()))); } catch (IOException e) { e.printStackTrace(); } } else { try { // if it's not left aligned then ignore list and list element and deal with it as normal text where
  • mimic
    behaviour float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tabBullet / 1000 * getFontSize()))); } catch (IOException e) { e.printStackTrace(); } } } textInLine.push(sinceLastWrapPoint); } if (isParagraph(token)) { // check if you have some text before this paragraph, if you don't then you really don't need extra line break for that if (textInLine.trimmedWidth() > 0) { // extra spacing because it's a paragraph result.add(" "); lineWidths.put(lineCounter, 0.0f); mapLineTokens.put(lineCounter, new ArrayList()); lineCounter++; } } else if (isListElement(token)) { listElement = true; // token padding, token bullet try { // if it's not left aligned then ignore list and list element and deal with it as normal text where
  • mimic
    behaviour float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB) : indentLevel(DEFAULT_TAB); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tab / 1000 * getFontSize()))); if (numberOfOrderedLists>0) { // if it's ordering list then move depending on your: ordering number + ". " String orderingNumber; if(listLevel > 1){ orderingNumber = stack.peek().getValue() + String.valueOf(orderListElement) + ". "; } else { orderingNumber = String.valueOf(orderListElement) + ". "; } textInLine.push(currentFont, fontSize, Token.text(TokenType.ORDERING, orderingNumber)); orderListElement++; } else { // if it's unordered list then just move by bullet character (take care of alignment!) textInLine.push(currentFont, fontSize, Token.text(TokenType.BULLET, " ")); } } catch (IOException e) { e.printStackTrace(); } } else { // wrapping at this must-have wrap point textInLine.push(sinceLastWrapPoint); result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; if(listLevel>0){ // preserve current indent try { if (numberOfOrderedLists>0) { float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0)) : indentLevel(DEFAULT_TAB); // if it's ordering list then move depending on your: ordering number + ". " String orderingNumber; if(listLevel > 1){ orderingNumber = stack.peek().getValue() + String.valueOf(orderListElement) + ". "; } else { orderingNumber = String.valueOf(orderListElement) + ". "; } float tabAndOrderingNumber = tab + font.getStringWidth(orderingNumber); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tabAndOrderingNumber / 1000 * getFontSize()))); orderListElement++; } else { if(getAlign().equals(HorizontalAlignment.LEFT)){ float tab = indentLevel(DEFAULT_TAB*Math.max(listLevel - 1, 0) + DEFAULT_TAB + BULLET_SPACE); textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tab / 1000 * getFontSize()))); } } } catch (IOException e) { e.printStackTrace(); } } } break; case TEXT: try { String word = token.getData(); float wordWidth = token.getWidth(currentFont); if(wordWidth / 1000f * fontSize > width && width > font.getAverageFontWidth() / 1000f * fontSize) { // you need to check if you have already something in your line boolean alreadyTextInLine = false; if(textInLine.trimmedWidth()>0){ alreadyTextInLine = true; } while (wordWidth / 1000f * fontSize > width) { float width = 0; float firstPartWordWidth = 0; float restOfTheWordWidth = 0; String lastTextToken = word; StringBuilder firstPartOfWord = new StringBuilder(); StringBuilder restOfTheWord = new StringBuilder(); for (int i = 0; i < lastTextToken.length(); i++) { char c = lastTextToken.charAt(i); try { width += (currentFont.getStringWidth(String.valueOf(c)) / 1000f * fontSize); } catch (IOException e) { e.printStackTrace(); } if(alreadyTextInLine){ if (width < this.width - textInLine.trimmedWidth()) { firstPartOfWord.append(c); firstPartWordWidth = Math.max(width, firstPartWordWidth); } else { restOfTheWord.append(c); restOfTheWordWidth = Math.max(width, restOfTheWordWidth); } } else { if (width < this.width) { firstPartOfWord.append(c); firstPartWordWidth = Math.max(width, firstPartWordWidth); } else { if(i==0){ firstPartOfWord.append(c); for (int j = 1; j< lastTextToken.length(); j++){ restOfTheWord.append(lastTextToken.charAt(j)); } break; } else { restOfTheWord.append(c); restOfTheWordWidth = Math.max(width, restOfTheWordWidth); } } } } // reset alreadyTextInLine = false; sinceLastWrapPoint.push(currentFont, fontSize, Token.text(TokenType.TEXT, firstPartOfWord.toString())); textInLine.push(sinceLastWrapPoint); // this is our line result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); textInLine.reset(); lineCounter++; word = restOfTheWord.toString(); wordWidth = currentFont.getStringWidth(word); } sinceLastWrapPoint.push(currentFont, fontSize, Token.text(TokenType.TEXT, word)); } else { sinceLastWrapPoint.push(currentFont, fontSize, token); } } catch (IOException e) { e.printStackTrace(); } break; } } if (sinceLastWrapPoint.trimmedWidth() + textInLine.trimmedWidth() > 0) { textInLine.push(sinceLastWrapPoint); result.add(textInLine.trimmedText()); lineWidths.put(lineCounter, textInLine.trimmedWidth()); mapLineTokens.put(lineCounter, textInLine.tokens()); maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth()); } lines = result; return result; } private static boolean isItalic(final Token token) { return "i".equals(token.getData()); } private static boolean isBold(final Token token) { return "b".equals(token.getData()); } private static boolean isParagraph(final Token token) { return "p".equals(token.getData()); } private static boolean isListElement(final Token token) { return "li".equals(token.getData()); } private static boolean isList(final Token token) { return "ul".equals(token.getData()) || "ol".equals(token.getData()); } private float indentLevel(int numberOfSpaces) throws IOException { if (spaceWidth == null) { spaceWidth = font.getSpaceWidth(); } return numberOfSpaces * spaceWidth; } public PDFont getFont(boolean isBold, boolean isItalic) { if (isBold) { if (isItalic) { return fontBoldItalic; } else { return fontBold; } } else if (isItalic) { return fontItalic; } else { return font; } } public float write(final PageContentStreamOptimized stream, float cursorX, float cursorY) { if (drawDebug) { PDStreamUtils.rectFontMetrics(stream, cursorX, cursorY, font, fontSize); // width PDStreamUtils.rect(stream, cursorX, cursorY, width, 1, Color.RED); } for (String line : getLines()) { line = line.trim(); float textX = cursorX; switch (align) { case CENTER: textX += getHorizontalFreeSpace(line) / 2; break; case LEFT: break; case RIGHT: textX += getHorizontalFreeSpace(line); break; } PDStreamUtils.write(stream, line, font, fontSize, textX, cursorY, color); if (textType != null) { switch (textType) { case HIGHLIGHT: case SQUIGGLY: case STRIKEOUT: throw new UnsupportedOperationException("Not implemented."); case UNDERLINE: float y = (float) (cursorY - FontUtils.getHeight(font, fontSize) - FontUtils.getDescent(font, fontSize) - 1.5); try { float titleWidth = font.getStringWidth(line) / 1000 * fontSize; stream.moveTo(textX, y); stream.lineTo(textX + titleWidth, y); stream.stroke(); } catch (final IOException e) { throw new IllegalStateException("Unable to underline text", e); } break; default: break; } } // move one "line" down cursorY -= getFontHeight(); } return cursorY; } public float getHeight() { if (getLines().size() == 0) { return 0; } else { return (getLines().size() - 1) * getLineSpacing() * getFontHeight() + getFontHeight(); } } public float getFontHeight() { return FontUtils.getHeight(font, fontSize); } /** * @deprecated This method will be removed in a future release * @return current font width */ @Deprecated public float getFontWidth() { return font.getFontDescriptor().getFontBoundingBox().getWidth() / 1000 * fontSize; } /** * @deprecated This method will be removed in a future release * @param width * Paragraph's width * @return {@link Paragraph} with designated width */ @Deprecated public Paragraph withWidth(int width) { invalidateLineWrapping(); this.width = width; return this; } /** * @deprecated This method will be removed in a future release * @param font * {@link PDFont} for {@link Paragraph} * @param fontSize * font size for {@link Paragraph} * @return {@link Paragraph} with designated font and font size */ @Deprecated public Paragraph withFont(PDFont font, int fontSize) { invalidateLineWrapping(); this.spaceWidth = null; this.font = font; this.fontSize = fontSize; return this; } // font, fontSize, width, and align are non-final and used in getLines(), // so if they are mutated, getLines() needs to be recomputed private void invalidateLineWrapping() { lines = null; } /** * /** * * @deprecated This method will be removed in a future release * @param color * {@code int} rgb value for color * @return Paragraph's {@link Color} */ @Deprecated public Paragraph withColor(int color) { this.color = new Color(color); return this; } /** * @deprecated This method will be replaced by * {@code public Color getColor()} in a future release * @return Paragraph's {@link Color} */ @Deprecated public int getColor() { return color.getRGB(); } private float getHorizontalFreeSpace(final String text) { try { final float tw = font.getStringWidth(text.trim()) / 1000 * fontSize; return width - tw; } catch (IOException e) { throw new IllegalStateException("Unable to calculate text width", e); } } public float getWidth() { return width; } public String getText() { return text; } public float getFontSize() { return fontSize; } public PDFont getFont() { return font; } public HorizontalAlignment getAlign() { return align; } public void setAlign(HorizontalAlignment align) { invalidateLineWrapping(); this.align = align; } public boolean isDrawDebug() { return drawDebug; } public void setDrawDebug(boolean drawDebug) { this.drawDebug = drawDebug; } public WrappingFunction getWrappingFunction() { return wrappingFunction == null ? DEFAULT_WRAP_FUNC : wrappingFunction; } public float getMaxLineWidth() { return maxLineWidth; } public float getLineWidth(int key) { return lineWidths.get(key); } public Map> getMapLineTokens() { return mapLineTokens; } public float getLineSpacing() { return lineSpacing; } public void setLineSpacing(float lineSpacing) { this.lineSpacing = lineSpacing; } }




  • © 2015 - 2025 Weber Informatics LLC | Privacy Policy