TextRenderer extends AbstractRenderer implements ILeafElementRenderer { @Override public LayoutResult layout(LayoutContext layoutContext) { updateFontAndText(); LayoutArea area = layoutContext.getArea(); Rectangle layoutBox = area.getBBox().clone(); boolean noSoftWrap = Boolean.TRUE.equals(this.parent.<Boolean>getOwnProperty(Property.NO_SOFT_WRAP_INLINE)); OverflowPropertyValue overflowX = this.parent.<OverflowPropertyValue>getProperty(Property.OVERFLOW_X); List<Rectangle> floatRendererAreas = layoutContext.getFloatRendererAreas(); FloatPropertyValue floatPropertyValue = this.<FloatPropertyValue>getProperty(Property.FLOAT); if (FloatingHelper.isRendererFloating(this, floatPropertyValue)) { FloatingHelper.adjustFloatedBlockLayoutBox(this, layoutBox, null, floatRendererAreas, floatPropertyValue, overflowX); } UnitValue[] margins = getMargins(); applyMargins(layoutBox, margins, false); Border[] borders = getBorders(); applyBorderBox(layoutBox, borders, false); UnitValue[] paddings = getPaddings(); applyPaddings(layoutBox, paddings, false); MinMaxWidth countedMinMaxWidth = new MinMaxWidth(area.getBBox().getWidth() - layoutBox.getWidth()); AbstractWidthHandler widthHandler; if (noSoftWrap) { widthHandler = new SumSumWidthHandler(countedMinMaxWidth); } else { widthHandler = new MaxSumWidthHandler(countedMinMaxWidth); } occupiedArea = new LayoutArea(area.getPageNumber(), new Rectangle(layoutBox.getX(), layoutBox.getY() + layoutBox.getHeight(), 0, 0)); boolean anythingPlaced = false; int currentTextPos = text.start; UnitValue fontSize = (UnitValue) this.getPropertyAsUnitValue(Property.FONT_SIZE); if (!fontSize.isPointValue()) { Logger logger = LoggerFactory.getLogger(TextRenderer.class); logger.error(MessageFormatUtil.format(LogMessageConstant.PROPERTY_IN_PERCENTS_NOT_SUPPORTED, Property.FONT_SIZE)); } float textRise = (float) this.getPropertyAsFloat(Property.TEXT_RISE); Float characterSpacing = this.getPropertyAsFloat(Property.CHARACTER_SPACING); Float wordSpacing = this.getPropertyAsFloat(Property.WORD_SPACING); float hScale = (float) this.getProperty(Property.HORIZONTAL_SCALING, (Float) 1f); ISplitCharacters splitCharacters = this.<ISplitCharacters>getProperty(Property.SPLIT_CHARACTERS); float italicSkewAddition = Boolean.TRUE.equals(getPropertyAsBoolean(Property.ITALIC_SIMULATION)) ? ITALIC_ANGLE * fontSize.getValue() : 0; float boldSimulationAddition = Boolean.TRUE.equals(getPropertyAsBoolean(Property.BOLD_SIMULATION)) ? BOLD_SIMULATION_STROKE_COEFF * fontSize.getValue() : 0; line = new GlyphLine(text); line.start = line.end = -1; float ascender = 0; float descender = 0; float currentLineAscender = 0; float currentLineDescender = 0; float currentLineHeight = 0; int initialLineTextPos = currentTextPos; float currentLineWidth = 0; int previousCharPos = -1; RenderingMode mode = this.<RenderingMode>getProperty(Property.RENDERING_MODE); float[] ascenderDescender = calculateAscenderDescender(font, mode); ascender = ascenderDescender[0]; descender = ascenderDescender[1]; if (RenderingMode.HTML_MODE.equals(mode)) { currentLineAscender = ascenderDescender[0]; currentLineDescender = ascenderDescender[1]; currentLineHeight = (currentLineAscender - currentLineDescender) * fontSize.getValue() / TEXT_SPACE_COEFF + textRise; } savedWordBreakAtLineEnding = null; Glyph wordBreakGlyphAtLineEnding = null; Character tabAnchorCharacter = this.<Character>getProperty(Property.TAB_ANCHOR); TextLayoutResult result = null; OverflowPropertyValue overflowY = !layoutContext.isClippedHeight() ? OverflowPropertyValue.FIT : this.parent.<OverflowPropertyValue>getProperty(Property.OVERFLOW_Y); boolean isSplitForcedByNewLine = false; boolean forcePartialSplitOnFirstChar = false; boolean ignoreNewLineSymbol = false; boolean crlf = false; HyphenationConfig hyphenationConfig = this.<HyphenationConfig>getProperty(Property.HYPHENATION); int firstPrintPos = currentTextPos; while (firstPrintPos < text.end && noPrint(text.get(firstPrintPos))) { firstPrintPos++; } while (currentTextPos < text.end) { if (noPrint(text.get(currentTextPos))) { if (line.start == -1) { line.start = currentTextPos; } line.end = Math.max(line.end, currentTextPos + 1); currentTextPos++; continue; } int nonBreakablePartEnd = text.end - 1; float nonBreakablePartFullWidth = 0; float nonBreakablePartWidthWhichDoesNotExceedAllowedWidth = 0; float nonBreakablePartMaxAscender = 0; float nonBreakablePartMaxDescender = 0; float nonBreakablePartMaxHeight = 0; int firstCharacterWhichExceedsAllowedWidth = -1; float nonBreakingHyphenRelatedChunkWidth = 0; int nonBreakingHyphenRelatedChunkStart = -1; float beforeNonBreakingHyphenRelatedChunkMaxAscender = 0; float beforeNonBreakingHyphenRelatedChunkMaxDescender = 0; for (int ind = currentTextPos; ind < text.end; ind++) { if (TextUtil.isNewLine(text.get(ind))) { wordBreakGlyphAtLineEnding = text.get(ind); isSplitForcedByNewLine = true; firstCharacterWhichExceedsAllowedWidth = ind + 1; if (ind != firstPrintPos) { ignoreNewLineSymbol = true; } else { forcePartialSplitOnFirstChar = true; } if (line.start == -1) { line.start = currentTextPos; } crlf = TextUtil.isCarriageReturnFollowedByLineFeed(text, currentTextPos); if (crlf) { currentTextPos++; } line.end = Math.max(line.end, firstCharacterWhichExceedsAllowedWidth - 1); break; } Glyph currentGlyph = text.get(ind); if (noPrint(currentGlyph)) { if (ind + 1 == text.end || splitCharacters.isSplitCharacter(text, ind + 1) && TextUtil.isSpaceOrWhitespace(text.get(ind + 1))) { nonBreakablePartEnd = ind; break; } continue; } if (tabAnchorCharacter != null && tabAnchorCharacter == text.get(ind).getUnicode()) { tabAnchorCharacterPosition = currentLineWidth + nonBreakablePartFullWidth; tabAnchorCharacter = null; } float glyphWidth = getCharWidth(currentGlyph, fontSize.getValue(), hScale, characterSpacing, wordSpacing) / TEXT_SPACE_COEFF; float xAdvance = previousCharPos != -1 ? text.get(previousCharPos).getXAdvance() : 0; if (xAdvance != 0) { xAdvance = scaleXAdvance(xAdvance, fontSize.getValue(), hScale) / TEXT_SPACE_COEFF; } if (!noSoftWrap && (nonBreakablePartFullWidth + glyphWidth + xAdvance + italicSkewAddition + boldSimulationAddition) > layoutBox.getWidth() - currentLineWidth && firstCharacterWhichExceedsAllowedWidth == -1 || ind == specialScriptFirstNotFittingIndex) { firstCharacterWhichExceedsAllowedWidth = ind; if (TextUtil.isSpaceOrWhitespace(text.get(ind))) { wordBreakGlyphAtLineEnding = currentGlyph; if (ind == firstPrintPos) { forcePartialSplitOnFirstChar = true; firstCharacterWhichExceedsAllowedWidth = ind + 1; break; } } } if (null != hyphenationConfig) { if (glyphBelongsToNonBreakingHyphenRelatedChunk(text, ind)) { if (-1 == nonBreakingHyphenRelatedChunkStart) { beforeNonBreakingHyphenRelatedChunkMaxAscender = nonBreakablePartMaxAscender; beforeNonBreakingHyphenRelatedChunkMaxDescender = nonBreakablePartMaxDescender; nonBreakingHyphenRelatedChunkStart = ind; } nonBreakingHyphenRelatedChunkWidth += glyphWidth + xAdvance; } else { nonBreakingHyphenRelatedChunkStart = -1; nonBreakingHyphenRelatedChunkWidth = 0; } } if (firstCharacterWhichExceedsAllowedWidth == -1) { nonBreakablePartWidthWhichDoesNotExceedAllowedWidth += glyphWidth + xAdvance; } nonBreakablePartFullWidth += glyphWidth + xAdvance; nonBreakablePartMaxAscender = Math.max(nonBreakablePartMaxAscender, ascender); nonBreakablePartMaxDescender = Math.min(nonBreakablePartMaxDescender, descender); nonBreakablePartMaxHeight = (nonBreakablePartMaxAscender - nonBreakablePartMaxDescender) * fontSize.getValue() / TEXT_SPACE_COEFF + textRise; previousCharPos = ind; if (!noSoftWrap && nonBreakablePartFullWidth + italicSkewAddition + boldSimulationAddition > layoutBox.getWidth() && (0 == nonBreakingHyphenRelatedChunkWidth || ind + 1 == text.end || !glyphBelongsToNonBreakingHyphenRelatedChunk(text, ind + 1))) { if (isOverflowFit(overflowX)) { break; } } boolean endOfWordBelongingToSpecialScripts = textContainsSpecialScriptGlyphs(true) && findPossibleBreaksSplitPosition(specialScriptsWordBreakPoints, ind + 1, true) >= 0; if (ind + 1 == text.end || splitCharacters.isSplitCharacter(text, ind) || splitCharacters.isSplitCharacter(text, ind + 1) && TextUtil.isSpaceOrWhitespace(text.get(ind + 1)) || endOfWordBelongingToSpecialScripts) { nonBreakablePartEnd = ind; break; } } if (firstCharacterWhichExceedsAllowedWidth == -1) { if (line.start == -1) { line.start = currentTextPos; } line.end = Math.max(line.end, nonBreakablePartEnd + 1); currentLineAscender = Math.max(currentLineAscender, nonBreakablePartMaxAscender); currentLineDescender = Math.min(currentLineDescender, nonBreakablePartMaxDescender); currentLineHeight = Math.max(currentLineHeight, nonBreakablePartMaxHeight); currentTextPos = nonBreakablePartEnd + 1; currentLineWidth += nonBreakablePartFullWidth; widthHandler.updateMinChildWidth(nonBreakablePartWidthWhichDoesNotExceedAllowedWidth + italicSkewAddition + boldSimulationAddition); widthHandler.updateMaxChildWidth(nonBreakablePartWidthWhichDoesNotExceedAllowedWidth + italicSkewAddition + boldSimulationAddition); anythingPlaced = true; } else { if (Math.max(currentLineHeight, nonBreakablePartMaxHeight) > layoutBox.getHeight() && isOverflowFit(overflowY)) { applyPaddings(occupiedArea.getBBox(), paddings, true); applyBorderBox(occupiedArea.getBBox(), borders, true); applyMargins(occupiedArea.getBBox(), margins, true); if (line.start == -1) { line.start = currentTextPos; } line.end = Math.max(line.end, firstCharacterWhichExceedsAllowedWidth); TextRenderer[] splitResult = split(initialLineTextPos); return new TextLayoutResult(LayoutResult.NOTHING, occupiedArea, splitResult[0], splitResult[1], this); } else { boolean wordSplit = false; boolean hyphenationApplied = false; if (hyphenationConfig != null) { if (-1 == nonBreakingHyphenRelatedChunkStart) { int[] wordBounds = getWordBoundsForHyphenation(text, currentTextPos, text.end, Math.max(currentTextPos, firstCharacterWhichExceedsAllowedWidth - 1)); if (wordBounds != null) { String word = text.toUnicodeString(wordBounds[0], wordBounds[1]); Hyphenation hyph = hyphenationConfig.hyphenate(word); if (hyph != null) { for (int i = hyph.length() - 1; i >= 0; i--) { String pre = hyph.getPreHyphenText(i); String pos = hyph.getPostHyphenText(i); float currentHyphenationChoicePreTextWidth = getGlyphLineWidth(convertToGlyphLine(text.toUnicodeString(currentTextPos, wordBounds[0]) + pre + hyphenationConfig.getHyphenSymbol()), fontSize.getValue(), hScale, characterSpacing, wordSpacing); if (currentLineWidth + currentHyphenationChoicePreTextWidth + italicSkewAddition + boldSimulationAddition <= layoutBox.getWidth()) { hyphenationApplied = true; if (line.start == -1) { line.start = currentTextPos; } line.end = Math.max(line.end, wordBounds[0] + pre.length()); GlyphLine lineCopy = line.copy(line.start, line.end); lineCopy.add(font.getGlyph(hyphenationConfig.getHyphenSymbol())); lineCopy.end++; line = lineCopy; currentLineAscender = Math.max(currentLineAscender, nonBreakablePartMaxAscender); currentLineDescender = Math.min(currentLineDescender, nonBreakablePartMaxDescender); currentLineHeight = Math.max(currentLineHeight, nonBreakablePartMaxHeight); currentLineWidth += currentHyphenationChoicePreTextWidth; widthHandler.updateMinChildWidth(currentHyphenationChoicePreTextWidth + italicSkewAddition + boldSimulationAddition); widthHandler.updateMaxChildWidth(currentHyphenationChoicePreTextWidth + italicSkewAddition + boldSimulationAddition); currentTextPos = wordBounds[0] + pre.length(); break; } } } } } else { if (text.start == nonBreakingHyphenRelatedChunkStart) { nonBreakingHyphenRelatedChunkWidth = 0; firstCharacterWhichExceedsAllowedWidth = previousCharPos + 1; } else { firstCharacterWhichExceedsAllowedWidth = nonBreakingHyphenRelatedChunkStart; nonBreakablePartFullWidth -= nonBreakingHyphenRelatedChunkWidth; nonBreakablePartMaxAscender = beforeNonBreakingHyphenRelatedChunkMaxAscender; nonBreakablePartMaxDescender = beforeNonBreakingHyphenRelatedChunkMaxDescender; } } } boolean specialScriptWordSplit = textContainsSpecialScriptGlyphs(true) && !isSplitForcedByNewLine; if ((nonBreakablePartFullWidth > layoutBox.getWidth() && !anythingPlaced && !hyphenationApplied) || forcePartialSplitOnFirstChar || -1 != nonBreakingHyphenRelatedChunkStart || specialScriptWordSplit) { if (line.start == -1) { line.start = currentTextPos; } if (!crlf) { currentTextPos = (forcePartialSplitOnFirstChar || isOverflowFit(overflowX) || specialScriptWordSplit) ? firstCharacterWhichExceedsAllowedWidth : nonBreakablePartEnd + 1; } line.end = Math.max(line.end, currentTextPos); wordSplit = !forcePartialSplitOnFirstChar && (text.end != currentTextPos); if (wordSplit || !(forcePartialSplitOnFirstChar || isOverflowFit(overflowX))) { currentLineAscender = Math.max(currentLineAscender, nonBreakablePartMaxAscender); currentLineDescender = Math.min(currentLineDescender, nonBreakablePartMaxDescender); currentLineHeight = Math.max(currentLineHeight, nonBreakablePartMaxHeight); currentLineWidth += nonBreakablePartWidthWhichDoesNotExceedAllowedWidth; widthHandler.updateMinChildWidth(nonBreakablePartWidthWhichDoesNotExceedAllowedWidth + italicSkewAddition + boldSimulationAddition); widthHandler.updateMaxChildWidth(nonBreakablePartWidthWhichDoesNotExceedAllowedWidth + italicSkewAddition + boldSimulationAddition); } else { currentLineAscender = ascender; currentLineDescender = descender; currentLineHeight = (currentLineAscender - currentLineDescender) * fontSize.getValue() / TEXT_SPACE_COEFF + textRise; currentLineWidth += getCharWidth(line.get(line.start), fontSize.getValue(), hScale, characterSpacing, wordSpacing) / TEXT_SPACE_COEFF; } } if (line.end <= line.start) { return new TextLayoutResult(LayoutResult.NOTHING, occupiedArea, null, this, this); } else { result = new TextLayoutResult(LayoutResult.PARTIAL, occupiedArea, null, null).setWordHasBeenSplit(wordSplit); } break; } } } boolean isPlacingForcedWhileNothing = false; if (currentLineHeight > layoutBox.getHeight()) { if (!Boolean.TRUE.equals(getPropertyAsBoolean(Property.FORCED_PLACEMENT)) && isOverflowFit(overflowY)) { applyPaddings(occupiedArea.getBBox(), paddings, true); applyBorderBox(occupiedArea.getBBox(), borders, true); applyMargins(occupiedArea.getBBox(), margins, true); return new TextLayoutResult(LayoutResult.NOTHING, occupiedArea, null, this, this); } else { isPlacingForcedWhileNothing = true; } } yLineOffset = currentLineAscender * fontSize.getValue() / TEXT_SPACE_COEFF; occupiedArea.getBBox().moveDown(currentLineHeight); occupiedArea.getBBox().setHeight(occupiedArea.getBBox().getHeight() + currentLineHeight); occupiedArea.getBBox().setWidth(Math.max(occupiedArea.getBBox().getWidth(), currentLineWidth)); layoutBox.setHeight(area.getBBox().getHeight() - currentLineHeight); occupiedArea.getBBox().setWidth(occupiedArea.getBBox().getWidth() + italicSkewAddition + boldSimulationAddition); applyPaddings(occupiedArea.getBBox(), paddings, true); applyBorderBox(occupiedArea.getBBox(), borders, true); applyMargins(occupiedArea.getBBox(), margins, true); increaseYLineOffset(paddings, borders, margins); if (result == null) { result = new TextLayoutResult(LayoutResult.FULL, occupiedArea, null, null, isPlacingForcedWhileNothing ? this : null); } else { TextRenderer[] split; if (ignoreNewLineSymbol || crlf) { split = splitIgnoreFirstNewLine(currentTextPos); } else { split = split(currentTextPos); } result.setSplitForcedByNewline(isSplitForcedByNewLine); result.setSplitRenderer(split[0]); if (wordBreakGlyphAtLineEnding != null) { split[0].saveWordBreakIfNotYetSaved(wordBreakGlyphAtLineEnding); } if (split[1].text.start != split[1].text.end) { result.setOverflowRenderer(split[1]); } else { result.setStatus(LayoutResult.FULL); } } if (FloatingHelper.isRendererFloating(this, floatPropertyValue)) { if (result.getStatus() == LayoutResult.FULL) { if (occupiedArea.getBBox().getWidth() > 0) { floatRendererAreas.add(occupiedArea.getBBox()); } } else if (result.getStatus() == LayoutResult.PARTIAL) { floatRendererAreas.add(result.getSplitRenderer().getOccupiedArea().getBBox()); } } result.setMinMaxWidth(countedMinMaxWidth); return result; } TextRenderer(Text textElement);  TextRenderer(Text textElement, String text); protected  TextRenderer(TextRenderer other); @Override LayoutResult layout(LayoutContext layoutContext); void applyOtf(); @Override void draw(DrawContext drawContext); void trimFirst(); @Override float getAscent(); @Override float getDescent(); float getYLine(); void moveYLineTo(float y); void setText(String text); @Deprecated void setText(GlyphLine text, int leftPos, int rightPos); void setText(GlyphLine text, PdfFont font); GlyphLine getText(); int length(); @Override String toString(); int charAt(int pos); float getTabAnchorCharacterPosition(); @Override IRenderer getNextRenderer(); static float[] calculateAscenderDescender(PdfFont font); static float[] calculateAscenderDescender(PdfFont font, RenderingMode mode); @Override MinMaxWidth getMinMaxWidth();  }
@Test public void nextRendererTest() { PdfDocument pdfDoc = new PdfDocument(new PdfWriter(new ByteArrayOutputStream())); pdfDoc.addNewPage(); Document doc = new Document(pdfDoc); RootRenderer documentRenderer = doc.getRenderer(); Text text = new Text("hello"); text.setNextRenderer(new TextRenderer(text)); IRenderer textRenderer1 = text.getRenderer().setParent(documentRenderer); IRenderer textRenderer2 = text.getRenderer().setParent(documentRenderer); LayoutArea area = new LayoutArea(1, new Rectangle(100, 100, 100, 100)); LayoutContext layoutContext = new LayoutContext(area); doc.close(); LayoutResult result1 = textRenderer1.layout(layoutContext); LayoutResult result2 = textRenderer2.layout(layoutContext); Assert.assertEquals(result1.getOccupiedArea(), result2.getOccupiedArea()); }