Java tutorial
/* * 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. */ /* $Id: TextLayoutManager.java 1297008 2012-03-05 11:19:47Z vhennebert $ */ package org.apache.fop.layoutmgr.inline; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.area.Trait; import org.apache.fop.area.inline.TextArea; import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; import org.apache.fop.complexscripts.util.CharScript; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FOText; import org.apache.fop.fonts.Font; import org.apache.fop.fonts.FontSelector; import org.apache.fop.layoutmgr.InlineKnuthSequence; import org.apache.fop.layoutmgr.KnuthBox; import org.apache.fop.layoutmgr.KnuthElement; import org.apache.fop.layoutmgr.KnuthGlue; import org.apache.fop.layoutmgr.KnuthPenalty; import org.apache.fop.layoutmgr.KnuthSequence; import org.apache.fop.layoutmgr.LayoutContext; import org.apache.fop.layoutmgr.LeafPosition; import org.apache.fop.layoutmgr.Position; import org.apache.fop.layoutmgr.PositionIterator; import org.apache.fop.layoutmgr.TraitSetter; import org.apache.fop.text.linebreak.LineBreakStatus; import org.apache.fop.traits.MinOptMax; import org.apache.fop.traits.SpaceVal; import org.apache.fop.util.CharUtilities; import org.apache.fop.util.ListUtil; /** * LayoutManager for text (a sequence of characters) which generates one * or more inline areas. */ public class TextLayoutManager extends LeafNodeLayoutManager { //TODO: remove all final modifiers at local variables // static final int SOFT_HYPHEN_PENALTY = KnuthPenalty.FLAGGED_PENALTY / 10; private static final int SOFT_HYPHEN_PENALTY = 1; /** * Store information about each potential text area. * Index of character which ends the area, IPD of area, including * any word-space and letter-space. * Number of word-spaces? */ private class AreaInfo { private final int startIndex; private final int breakIndex; private int wordCharLength; private final int wordSpaceCount; private int letterSpaceCount; private MinOptMax areaIPD; private final boolean isHyphenated; private final boolean isSpace; private boolean breakOppAfter; private final Font font; private final int level; private final int[][] gposAdjustments; AreaInfo // CSOK: ParameterNumber (int startIndex, int breakIndex, int wordSpaceCount, int letterSpaceCount, MinOptMax areaIPD, boolean isHyphenated, boolean isSpace, boolean breakOppAfter, Font font, int level, int[][] gposAdjustments) { assert startIndex <= breakIndex; this.startIndex = startIndex; this.breakIndex = breakIndex; this.wordCharLength = -1; this.wordSpaceCount = wordSpaceCount; this.letterSpaceCount = letterSpaceCount; this.areaIPD = areaIPD; this.isHyphenated = isHyphenated; this.isSpace = isSpace; this.breakOppAfter = breakOppAfter; this.font = font; this.level = level; this.gposAdjustments = gposAdjustments; } /** * Obtain number of 'characters' contained in word. If word * is mapped, then this number may be less than or greater than the * original length (breakIndex - startIndex). We compute and * memoize thius length upon first invocation of this method. */ private int getWordLength() { if (wordCharLength == -1) { if (foText.hasMapping(startIndex, breakIndex)) { wordCharLength = foText.getMapping(startIndex, breakIndex).length(); } else { assert breakIndex >= startIndex; wordCharLength = breakIndex - startIndex; } } return wordCharLength; } private void addToAreaIPD(MinOptMax idp) { areaIPD = areaIPD.plus(idp); } public String toString() { return super.toString() + "{" + "interval = [" + startIndex + "," + breakIndex + "]" + ", isSpace = " + isSpace + ", level = " + level + ", areaIPD = " + areaIPD + ", letterSpaceCount = " + letterSpaceCount + ", wordSpaceCount = " + wordSpaceCount + ", isHyphenated = " + isHyphenated + ", font = " + font + "}"; } } /** * this class stores information about changes in vecAreaInfo which are not yet applied */ private final class PendingChange { private final AreaInfo areaInfo; private final int index; private PendingChange(final AreaInfo areaInfo, final int index) { this.areaInfo = areaInfo; this.index = index; } } /** * logging instance */ private static final Log LOG = LogFactory.getLog(TextLayoutManager.class); // Hold all possible breaks for the text in this LM's FO. private final List areaInfos; /** Non-space characters on which we can end a line. */ private static final String BREAK_CHARS = "-/"; private final FOText foText; /** * Contains an array of widths to adjust for kerning. The first entry can * be used to influence the start position of the first letter. The entry i+1 defines the * cursor advancement after the character i. A null entry means no special advancement. */ private final MinOptMax[] letterSpaceAdjustArray; //size = textArray.length + 1 /** Font used for the space between words. */ private Font spaceFont = null; /** Start index of next TextArea */ private int nextStart = 0; /** size of a space character (U+0020) glyph in current font */ private int spaceCharIPD; private MinOptMax wordSpaceIPD; private MinOptMax letterSpaceIPD; /** size of the hyphen character glyph in current font */ private int hyphIPD; private boolean hasChanged = false; private int[] returnedIndices = { 0, 0 }; private int changeOffset = 0; private int thisStart = 0; private int tempStart = 0; private List changeList = new LinkedList(); private AlignmentContext alignmentContext = null; /** * The width to be reserved for border and padding at the start of the line. */ private int lineStartBAP = 0; /** * The width to be reserved for border and padding at the end of the line. */ private int lineEndBAP = 0; private boolean keepTogether; private final Position auxiliaryPosition = new LeafPosition(this, -1); /** * Create a Text layout manager. * * @param node The FOText object to be rendered */ public TextLayoutManager(FOText node) { foText = node; letterSpaceAdjustArray = new MinOptMax[node.length() + 1]; areaInfos = new ArrayList(); } private KnuthPenalty makeZeroWidthPenalty(int penaltyValue) { return new KnuthPenalty(0, penaltyValue, false, auxiliaryPosition, true); } private KnuthBox makeAuxiliaryZeroWidthBox() { return new KnuthInlineBox(0, null, notifyPos(new LeafPosition(this, -1)), true); } /** {@inheritDoc} */ public void initialize() { foText.resetBuffer(); spaceFont = FontSelector.selectFontForCharacterInText(' ', foText, this); // With CID fonts, space isn't necessary currentFontState.width(32) spaceCharIPD = spaceFont.getCharWidth(' '); // Use hyphenationChar property // TODO: Use hyphen based on actual font used! hyphIPD = foText.getCommonHyphenation().getHyphIPD(spaceFont); SpaceVal letterSpacing = SpaceVal.makeLetterSpacing(foText.getLetterSpacing()); SpaceVal wordSpacing = SpaceVal.makeWordSpacing(foText.getWordSpacing(), letterSpacing, spaceFont); // letter space applies only to consecutive non-space characters, // while word space applies to space characters; // i.e. the spaces in the string "A SIMPLE TEST" are: // A<<ws>>S<ls>I<ls>M<ls>P<ls>L<ls>E<<ws>>T<ls>E<ls>S<ls>T // there is no letter space after the last character of a word, // nor after a space character // NOTE: The above is not quite correct. Read on in XSL 1.0, 7.16.2, letter-spacing // set letter space and word space dimension; // the default value "normal" was converted into a MinOptMax value // in the SpaceVal.makeWordSpacing() method letterSpaceIPD = letterSpacing.getSpace(); wordSpaceIPD = MinOptMax.getInstance(spaceCharIPD).plus(wordSpacing.getSpace()); keepTogether = foText.getKeepTogether().getWithinLine().getEnum() == Constants.EN_ALWAYS; } /** * Generate and add areas to parent area. * This can either generate an area for each TextArea and each space, or * an area containing all text with a parameter controlling the size of * the word space. The latter is most efficient for PDF generation. * Set size of each area. * @param posIter Iterator over Position information returned * by this LayoutManager. * @param context LayoutContext for adjustments */ public void addAreas(final PositionIterator posIter, final LayoutContext context) { // Add word areas AreaInfo areaInfo; int wordSpaceCount = 0; int letterSpaceCount = 0; int firstAreaInfoIndex = -1; int lastAreaInfoIndex = 0; MinOptMax realWidth = MinOptMax.ZERO; /* On first area created, add any leading space. * Calculate word-space stretch value. */ AreaInfo lastAreaInfo = null; while (posIter.hasNext()) { final LeafPosition tbpNext = (LeafPosition) posIter.next(); if (tbpNext == null) { continue; //Ignore elements without Positions } if (tbpNext.getLeafPos() != -1) { areaInfo = (AreaInfo) areaInfos.get(tbpNext.getLeafPos()); if (lastAreaInfo == null || (areaInfo.font != lastAreaInfo.font) || (areaInfo.level != lastAreaInfo.level)) { if (lastAreaInfo != null) { addAreaInfoAreas(lastAreaInfo, wordSpaceCount, letterSpaceCount, firstAreaInfoIndex, lastAreaInfoIndex, realWidth, context); } firstAreaInfoIndex = tbpNext.getLeafPos(); wordSpaceCount = 0; letterSpaceCount = 0; realWidth = MinOptMax.ZERO; } wordSpaceCount += areaInfo.wordSpaceCount; letterSpaceCount += areaInfo.letterSpaceCount; realWidth = realWidth.plus(areaInfo.areaIPD); lastAreaInfoIndex = tbpNext.getLeafPos(); lastAreaInfo = areaInfo; } } if (lastAreaInfo != null) { addAreaInfoAreas(lastAreaInfo, wordSpaceCount, letterSpaceCount, firstAreaInfoIndex, lastAreaInfoIndex, realWidth, context); } } private void addAreaInfoAreas(AreaInfo areaInfo, int wordSpaceCount, int letterSpaceCount, int firstAreaInfoIndex, int lastAreaInfoIndex, MinOptMax realWidth, LayoutContext context) { // TODO: These two statements (if, for) were like this before my recent // changes. However, it seems as if they should use the AreaInfo from // firstAreaInfoIndex.. lastAreaInfoIndex rather than just the last areaInfo. // This needs to be checked. int textLength = areaInfo.getWordLength(); if (areaInfo.letterSpaceCount == textLength && !areaInfo.isHyphenated && context.isLastArea()) { // the line ends at a character like "/" or "-"; // remove the letter space after the last character realWidth = realWidth.minus(letterSpaceIPD); letterSpaceCount--; } for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { MinOptMax letterSpaceAdjustment = letterSpaceAdjustArray[i + 1]; if (letterSpaceAdjustment != null && letterSpaceAdjustment.isElastic()) { letterSpaceCount++; } } // add hyphenation character if the last word is hyphenated if (context.isLastArea() && areaInfo.isHyphenated) { realWidth = realWidth.plus(hyphIPD); } /* Calculate adjustments */ double ipdAdjust = context.getIPDAdjust(); // calculate total difference between real and available width int difference; if (ipdAdjust > 0.0) { difference = (int) (realWidth.getStretch() * ipdAdjust); } else { difference = (int) (realWidth.getShrink() * ipdAdjust); } // set letter space adjustment int letterSpaceDim = letterSpaceIPD.getOpt(); if (ipdAdjust > 0.0) { letterSpaceDim += (int) (letterSpaceIPD.getStretch() * ipdAdjust); } else { letterSpaceDim += (int) (letterSpaceIPD.getShrink() * ipdAdjust); } int totalAdjust = (letterSpaceDim - letterSpaceIPD.getOpt()) * letterSpaceCount; // set word space adjustment int wordSpaceDim = wordSpaceIPD.getOpt(); if (wordSpaceCount > 0) { wordSpaceDim += (difference - totalAdjust) / wordSpaceCount; } totalAdjust += (wordSpaceDim - wordSpaceIPD.getOpt()) * wordSpaceCount; if (totalAdjust != difference) { // the applied adjustment is greater or smaller than the needed one TextLayoutManager.LOG.trace( "TextLM.addAreas: error in word / letter space adjustment = " + (totalAdjust - difference)); // set totalAdjust = difference, so that the width of the TextArea // will counterbalance the error and the other inline areas will be // placed correctly totalAdjust = difference; } TextArea textArea = new TextAreaBuilder(realWidth, totalAdjust, context, firstAreaInfoIndex, lastAreaInfoIndex, context.isLastArea(), areaInfo.font).build(); // wordSpaceDim is computed in relation to wordSpaceIPD.opt // but the renderer needs to know the adjustment in relation // to the size of the space character in the current font; // moreover, the pdf renderer adds the character spacing even to // the last character of a word and to space characters: in order // to avoid this, we must subtract the letter space width twice; // the renderer will compute the space width as: // space width = // = "normal" space width + letterSpaceAdjust + wordSpaceAdjust // = spaceCharIPD + letterSpaceAdjust + // + (wordSpaceDim - spaceCharIPD - 2 * letterSpaceAdjust) // = wordSpaceDim - letterSpaceAdjust textArea.setTextLetterSpaceAdjust(letterSpaceDim); textArea.setTextWordSpaceAdjust(wordSpaceDim - spaceCharIPD - 2 * textArea.getTextLetterSpaceAdjust()); if (context.getIPDAdjust() != 0) { // add information about space width textArea.setSpaceDifference( wordSpaceIPD.getOpt() - spaceCharIPD - 2 * textArea.getTextLetterSpaceAdjust()); } parentLayoutManager.addChildArea(textArea); } private final class TextAreaBuilder { // constructor initialized state private final MinOptMax width; // content ipd private final int adjust; // content ipd adjustment private final LayoutContext context; // layout context private final int firstIndex; // index of first AreaInfo private final int lastIndex; // index of last AreaInfo private final boolean isLastArea; // true if last inline area in line area private final Font font; // applicable font // other, non-constructor state private TextArea textArea; // text area being constructed private int blockProgressionDimension; // calculated bpd private AreaInfo areaInfo; // current area info when iterating over words private StringBuffer wordChars; // current word's character buffer private int[] letterSpaceAdjust; // current word's letter space adjustments private int letterSpaceAdjustIndex; // last written letter space adjustment index private int[] wordLevels; // current word's bidi levels private int wordLevelsIndex; // last written bidi level index private int wordIPD; // accumulated ipd of current word private int[][] gposAdjustments; // current word's glyph position adjustments private int gposAdjustmentsIndex; // last written glyph position adjustment index /** * Creates a new <code>TextAreaBuilder</code> which itself builds an inline word area. This * creates a TextArea and sets up the various attributes. * * @param width the MinOptMax width of the content * @param adjust the total ipd adjustment with respect to the optimal width * @param context the layout context * @param firstIndex the index of the first AreaInfo used for the TextArea * @param lastIndex the index of the last AreaInfo used for the TextArea * @param isLastArea is this TextArea the last in a line? * @param font Font to be used in this particular TextArea */ private TextAreaBuilder(MinOptMax width, int adjust, LayoutContext context, int firstIndex, int lastIndex, boolean isLastArea, Font font) { this.width = width; this.adjust = adjust; this.context = context; this.firstIndex = firstIndex; this.lastIndex = lastIndex; this.isLastArea = isLastArea; this.font = font; } private TextArea build() { createTextArea(); setInlineProgressionDimension(); calcBlockProgressionDimension(); setBlockProgressionDimension(); setBaselineOffset(); setBlockProgressionOffset(); setText(); TraitSetter.addFontTraits(textArea, font); textArea.addTrait(Trait.COLOR, foText.getColor()); TraitSetter.addTextDecoration(textArea, foText.getTextDecoration()); TraitSetter.addStructureTreeElement(textArea, foText.getStructureTreeElement()); return textArea; } /** * Creates an plain <code>TextArea</code> or a justified <code>TextArea</code> with * additional information. */ private void createTextArea() { if (context.getIPDAdjust() == 0.0) { textArea = new TextArea(); } else { textArea = new TextArea(width.getStretch(), width.getShrink(), adjust); } } private void setInlineProgressionDimension() { textArea.setIPD(width.getOpt() + adjust); } private void calcBlockProgressionDimension() { blockProgressionDimension = font.getAscender() - font.getDescender(); } private void setBlockProgressionDimension() { textArea.setBPD(blockProgressionDimension); } private void setBaselineOffset() { textArea.setBaselineOffset(font.getAscender()); } private void setBlockProgressionOffset() { if (blockProgressionDimension == alignmentContext.getHeight()) { textArea.setBlockProgressionOffset(0); } else { textArea.setBlockProgressionOffset(alignmentContext.getOffset()); } } /** * Sets the text of the TextArea, split into words and spaces. */ private void setText() { int areaInfoIndex = -1; int wordCharLength = 0; for (int wordIndex = firstIndex; wordIndex <= lastIndex; wordIndex++) { areaInfo = getAreaInfo(wordIndex); if (areaInfo.isSpace) { addSpaces(); } else { // areaInfo stores information about a word fragment if (areaInfoIndex == -1) { // here starts a new word areaInfoIndex = wordIndex; wordCharLength = 0; } wordCharLength += areaInfo.getWordLength(); if (isWordEnd(wordIndex)) { addWord(areaInfoIndex, wordIndex, wordCharLength); areaInfoIndex = -1; } } } } private boolean isWordEnd(int areaInfoIndex) { return areaInfoIndex == lastIndex || getAreaInfo(areaInfoIndex + 1).isSpace; } /** * Add word with fragments from STARTINDEX to ENDINDEX, where * total length of (possibly mapped) word is CHARLENGTH. * A word is composed from one or more word fragments, where each * fragment corresponds to distinct instance in a sequence of * area info instances starting at STARTINDEX continuing through (and * including) ENDINDEX. * @param startIndex index of first area info of word to add * @param endIndex index of last area info of word to add * @param wordLength number of (mapped) characters in word */ private void addWord(int startIndex, int endIndex, int wordLength) { int blockProgressionOffset = 0; boolean gposAdjusted = false; if (isHyphenated(endIndex)) { // TODO may be problematic in some I18N contexts [GA] wordLength++; } initWord(wordLength); // iterate over word's fragments, adding word chars (with bidi // levels), letter space adjustments, and glyph position adjustments for (int i = startIndex; i <= endIndex; i++) { AreaInfo wordAreaInfo = getAreaInfo(i); addWordChars(wordAreaInfo); addLetterAdjust(wordAreaInfo); if (addGlyphPositionAdjustments(wordAreaInfo)) { gposAdjusted = true; } } if (isHyphenated(endIndex)) { // TODO may be problematic in some I18N contexts [GA] addHyphenationChar(); } if (!gposAdjusted) { gposAdjustments = null; } textArea.addWord(wordChars.toString(), wordIPD, letterSpaceAdjust, getNonEmptyLevels(), gposAdjustments, blockProgressionOffset); } private int[] getNonEmptyLevels() { if (wordLevels != null) { assert wordLevelsIndex <= wordLevels.length; boolean empty = true; for (int i = 0, n = wordLevelsIndex; i < n; i++) { if (wordLevels[i] >= 0) { empty = false; break; } } return empty ? null : wordLevels; } else { return null; } } /** * Fully allocate word character buffer, letter space adjustments * array, bidi levels array, and glyph position adjustments array. * based on full word length, including all (possibly mapped) fragments. * @param wordLength length of word including all (possibly mapped) fragments */ private void initWord(int wordLength) { wordChars = new StringBuffer(wordLength); letterSpaceAdjust = new int[wordLength]; letterSpaceAdjustIndex = 0; wordLevels = new int[wordLength]; wordLevelsIndex = 0; Arrays.fill(wordLevels, -1); gposAdjustments = new int[wordLength][4]; gposAdjustmentsIndex = 0; wordIPD = 0; } private boolean isHyphenated(int endIndex) { return isLastArea && endIndex == lastIndex && areaInfo.isHyphenated; } private void addHyphenationChar() { wordChars.append(foText.getCommonHyphenation().getHyphChar(font)); // [TBD] expand bidi word levels, letter space adjusts, gpos adjusts // [TBD] [GA] problematic in bidi context... what is level of hyphen? } /** * Given a word area info associated with a word fragment, * (1) concatenate (possibly mapped) word characters to word character buffer; * (2) concatenante (possibly mapped) word bidi levels to levels buffer; * (3) update word's IPD with optimal IPD of fragment. * @param wordAreaInfo fragment info */ private void addWordChars(AreaInfo wordAreaInfo) { int s = wordAreaInfo.startIndex; int e = wordAreaInfo.breakIndex; if (foText.hasMapping(s, e)) { wordChars.append(foText.getMapping(s, e)); addWordLevels(foText.getMappingBidiLevels(s, e)); } else { for (int i = s; i < e; i++) { wordChars.append(foText.charAt(i)); } addWordLevels(foText.getBidiLevels(s, e)); } wordIPD += wordAreaInfo.areaIPD.getOpt(); } /** * Given a (possibly null) bidi levels array associated with a word fragment, * concatenante (possibly mapped) word bidi levels to levels buffer. * @param levels bidi levels array or null */ private void addWordLevels(int[] levels) { int numLevels = (levels != null) ? levels.length : 0; if (numLevels > 0) { int need = wordLevelsIndex + numLevels; if (need <= wordLevels.length) { System.arraycopy(levels, 0, wordLevels, wordLevelsIndex, numLevels); } else { throw new IllegalStateException("word levels array too short: expect at least " + need + " entries, but has only " + wordLevels.length + " entries"); } } wordLevelsIndex += numLevels; } /** * Given a word area info associated with a word fragment, * concatenate letter space adjustments for each (possibly mapped) character. * @param wordAreaInfo fragment info */ private void addLetterAdjust(AreaInfo wordAreaInfo) { int letterSpaceCount = wordAreaInfo.letterSpaceCount; int wordLength = wordAreaInfo.getWordLength(); int taAdjust = textArea.getTextLetterSpaceAdjust(); for (int i = 0, n = wordLength; i < n; i++) { int j = letterSpaceAdjustIndex + i; if (j > 0) { int k = wordAreaInfo.startIndex + i; MinOptMax adj = (k < letterSpaceAdjustArray.length) ? letterSpaceAdjustArray[k] : null; letterSpaceAdjust[j] = (adj == null) ? 0 : adj.getOpt(); } if (letterSpaceCount > 0) { letterSpaceAdjust[j] += taAdjust; letterSpaceCount--; } } letterSpaceAdjustIndex += wordLength; } /** * Given a word area info associated with a word fragment, * concatenate glyph position adjustments for each (possibly mapped) character. * @param wordAreaInfo fragment info * @return true if an adjustment was non-zero */ private boolean addGlyphPositionAdjustments(AreaInfo wordAreaInfo) { boolean adjusted = false; int[][] gpa = wordAreaInfo.gposAdjustments; int numAdjusts = (gpa != null) ? gpa.length : 0; int wordLength = wordAreaInfo.getWordLength(); if (numAdjusts > 0) { int need = gposAdjustmentsIndex + numAdjusts; if (need <= gposAdjustments.length) { for (int i = 0, n = wordLength, j = 0; i < n; i++) { if (i < numAdjusts) { int[] wpa1 = gposAdjustments[gposAdjustmentsIndex + i]; int[] wpa2 = gpa[j++]; for (int k = 0; k < 4; k++) { int a = wpa2[k]; if (a != 0) { wpa1[k] += a; adjusted = true; } } } } } else { throw new IllegalStateException("gpos adjustments array too short: expect at least " + need + " entries, but has only " + gposAdjustments.length + " entries"); } } gposAdjustmentsIndex += wordLength; return adjusted; } /** * The <code>AreaInfo</code> stores information about spaces. * <p/> * Add the spaces - except zero-width spaces - to the TextArea. */ private void addSpaces() { int blockProgressionOffset = 0; // [TBD] need to better handling of spaceIPD assignment, for now, // divide the area info's allocated IPD evenly among the // non-zero-width space characters int numZeroWidthSpaces = 0; for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { char spaceChar = foText.charAt(i); if (CharUtilities.isZeroWidthSpace(spaceChar)) { numZeroWidthSpaces++; } } int numSpaces = areaInfo.breakIndex - areaInfo.startIndex - numZeroWidthSpaces; int spaceIPD = areaInfo.areaIPD.getOpt() / ((numSpaces > 0) ? numSpaces : 1); // add space area children, one for each non-zero-width space character for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { char spaceChar = foText.charAt(i); int level = foText.bidiLevelAt(i); if (!CharUtilities.isZeroWidthSpace(spaceChar)) { textArea.addSpace(spaceChar, spaceIPD, CharUtilities.isAdjustableSpace(spaceChar), blockProgressionOffset, level); } } } } private void addAreaInfo(AreaInfo ai) { addAreaInfo(areaInfos.size(), ai); } private void addAreaInfo(int index, AreaInfo ai) { areaInfos.add(index, ai); } private void removeAreaInfo(int index) { areaInfos.remove(index); } private AreaInfo getAreaInfo(int index) { return (AreaInfo) areaInfos.get(index); } private void addToLetterAdjust(int index, int width) { if (letterSpaceAdjustArray[index] == null) { letterSpaceAdjustArray[index] = MinOptMax.getInstance(width); } else { letterSpaceAdjustArray[index] = letterSpaceAdjustArray[index].plus(width); } } /** * Indicates whether a character is a space in terms of this layout manager. * @param ch the character * @return true if it's a space */ private static boolean isSpace(final char ch) { return ch == CharUtilities.SPACE || CharUtilities.isNonBreakableSpace(ch) || CharUtilities.isFixedWidthSpace(ch); } /** {@inheritDoc} */ public List getNextKnuthElements(final LayoutContext context, final int alignment) { lineStartBAP = context.getLineStartBorderAndPaddingWidth(); lineEndBAP = context.getLineEndBorderAndPaddingWidth(); alignmentContext = context.getAlignmentContext(); final List returnList = new LinkedList(); KnuthSequence sequence = new InlineKnuthSequence(); AreaInfo areaInfo = null; AreaInfo prevAreaInfo = null; returnList.add(sequence); if (LOG.isDebugEnabled()) { LOG.debug("GK: [" + nextStart + "," + foText.length() + "]"); } LineBreakStatus lineBreakStatus = new LineBreakStatus(); thisStart = nextStart; boolean inWord = false; boolean inWhitespace = false; char ch = 0; int level = -1; int prevLevel = -1; while (nextStart < foText.length()) { ch = foText.charAt(nextStart); level = foText.bidiLevelAt(nextStart); boolean breakOpportunity = false; byte breakAction = keepTogether ? LineBreakStatus.PROHIBITED_BREAK : lineBreakStatus.nextChar(ch); switch (breakAction) { case LineBreakStatus.COMBINING_PROHIBITED_BREAK: case LineBreakStatus.PROHIBITED_BREAK: break; case LineBreakStatus.EXPLICIT_BREAK: break; case LineBreakStatus.COMBINING_INDIRECT_BREAK: case LineBreakStatus.DIRECT_BREAK: case LineBreakStatus.INDIRECT_BREAK: breakOpportunity = true; break; default: TextLayoutManager.LOG.error("Unexpected breakAction: " + breakAction); } if (LOG.isDebugEnabled()) { LOG.debug("GK: {" + " index = " + nextStart + ", char = " + CharUtilities.charToNCRef(ch) + ", level = " + level + ", levelPrev = " + prevLevel + ", inWord = " + inWord + ", inSpace = " + inWhitespace + "}"); } if (inWord) { if (breakOpportunity || TextLayoutManager.isSpace(ch) || CharUtilities.isExplicitBreak(ch) || ((prevLevel != -1) && (level != prevLevel))) { // this.foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN prevAreaInfo = processWord(alignment, sequence, prevAreaInfo, ch, breakOpportunity, true, prevLevel); } } else if (inWhitespace) { if (ch != CharUtilities.SPACE || breakOpportunity) { prevAreaInfo = processWhitespace(alignment, sequence, breakOpportunity, prevLevel); } } else { if (areaInfo != null) { prevAreaInfo = areaInfo; processLeftoverAreaInfo(alignment, sequence, areaInfo, ch == CharUtilities.SPACE || breakOpportunity); areaInfo = null; } if (breakAction == LineBreakStatus.EXPLICIT_BREAK) { sequence = processLinebreak(returnList, sequence); } } if (ch == CharUtilities.SPACE && foText.getWhitespaceTreatment() == Constants.EN_PRESERVE || ch == CharUtilities.NBSPACE) { // preserved space or non-breaking space: // create the AreaInfo object areaInfo = new AreaInfo(nextStart, nextStart + 1, 1, 0, wordSpaceIPD, false, true, breakOpportunity, spaceFont, level, null); thisStart = nextStart + 1; } else if (CharUtilities.isFixedWidthSpace(ch) || CharUtilities.isZeroWidthSpace(ch)) { // create the AreaInfo object Font font = FontSelector.selectFontForCharacterInText(ch, foText, this); MinOptMax ipd = MinOptMax.getInstance(font.getCharWidth(ch)); areaInfo = new AreaInfo(nextStart, nextStart + 1, 0, 0, ipd, false, true, breakOpportunity, font, level, null); thisStart = nextStart + 1; } else if (CharUtilities.isExplicitBreak(ch)) { //mandatory break-character: only advance index thisStart = nextStart + 1; } inWord = !TextLayoutManager.isSpace(ch) && !CharUtilities.isExplicitBreak(ch); inWhitespace = ch == CharUtilities.SPACE && foText.getWhitespaceTreatment() != Constants.EN_PRESERVE; prevLevel = level; nextStart++; } // Process any last elements if (inWord) { processWord(alignment, sequence, prevAreaInfo, ch, false, false, prevLevel); } else if (inWhitespace) { processWhitespace(alignment, sequence, !keepTogether, prevLevel); } else if (areaInfo != null) { processLeftoverAreaInfo(alignment, sequence, areaInfo, ch == CharUtilities.ZERO_WIDTH_SPACE); } else if (CharUtilities.isExplicitBreak(ch)) { this.processLinebreak(returnList, sequence); } if (((List) ListUtil.getLast(returnList)).isEmpty()) { //Remove an empty sequence because of a trailing newline ListUtil.removeLast(returnList); } setFinished(true); if (returnList.isEmpty()) { return null; } else { return returnList; } } private KnuthSequence processLinebreak(List returnList, KnuthSequence sequence) { if (lineEndBAP != 0) { sequence.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, true)); } sequence.endSequence(); sequence = new InlineKnuthSequence(); returnList.add(sequence); return sequence; } private void processLeftoverAreaInfo(int alignment, KnuthSequence sequence, AreaInfo areaInfo, boolean breakOpportunityAfter) { addAreaInfo(areaInfo); areaInfo.breakOppAfter = breakOpportunityAfter; addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1); } private AreaInfo processWhitespace(final int alignment, final KnuthSequence sequence, final boolean breakOpportunity, int level) { if (LOG.isDebugEnabled()) { LOG.debug("PS: [" + thisStart + "," + nextStart + "]"); } // End of whitespace // create the AreaInfo object assert nextStart >= thisStart; AreaInfo areaInfo = new AreaInfo(thisStart, nextStart, nextStart - thisStart, 0, wordSpaceIPD.mult(nextStart - thisStart), false, true, breakOpportunity, spaceFont, level, null); addAreaInfo(areaInfo); // create the elements addElementsForASpace(sequence, alignment, areaInfo, areaInfos.size() - 1); thisStart = nextStart; return areaInfo; } private AreaInfo processWordMapping(int lastIndex, final Font font, AreaInfo prevAreaInfo, final char breakOpportunityChar, final boolean endsWithHyphen, int level) { int s = this.thisStart; // start index of word in FOText character buffer int e = lastIndex; // end index of word in FOText character buffer int nLS = 0; // # of letter spaces String script = foText.getScript(); String language = foText.getLanguage(); if (LOG.isDebugEnabled()) { LOG.debug("PW: [" + thisStart + "," + lastIndex + "]: {" + " +M" + ", level = " + level + " }"); } // 1. extract unmapped character sequence CharSequence ics = foText.subSequence(s, e); // 2. if script is not specified (by FO property) or it is specified as 'auto', // then compute dominant script if ((script == null) || "auto".equals(script)) { script = CharScript.scriptTagFromCode(CharScript.dominantScript(ics)); } if ((language == null) || "none".equals(language)) { language = "dflt"; } // 3. perform mapping of chars to glyphs ... to glyphs ... to chars CharSequence mcs = font.performSubstitution(ics, script, language); // 4. compute glyph position adjustments on (substituted) characters int[][] gpa; if (font.performsPositioning()) { // handle GPOS adjustments gpa = font.performPositioning(mcs, script, language); } else if (font.hasKerning()) { // handle standard (non-GPOS) kerning adjustments gpa = getKerningAdjustments(mcs, font); } else { gpa = null; } // 5. reorder combining marks so that they precede (within the mapped char sequence) the // base to which they are applied; N.B. position adjustments (gpa) are reordered in place mcs = font.reorderCombiningMarks(mcs, gpa, script, language); // 6. if mapped sequence differs from input sequence, then memoize mapped sequence if (!CharUtilities.isSameSequence(mcs, ics)) { foText.addMapping(s, e, mcs); } // 7. compute word ipd based on final position adjustments MinOptMax ipd = MinOptMax.ZERO; for (int i = 0, n = mcs.length(); i < n; i++) { int c = mcs.charAt(i); // TODO !BMP int w = font.getCharWidth(c); if (w < 0) { w = 0; } if (gpa != null) { w += gpa[i][GlyphPositioningTable.Value.IDX_X_ADVANCE]; } ipd = ipd.plus(w); } // [TBD] - handle letter spacing return new AreaInfo(s, e, 0, nLS, ipd, endsWithHyphen, false, breakOpportunityChar != 0, font, level, gpa); } /** * Given a mapped character sequence MCS, obtain glyph position adjustments * from the font's kerning data. * @param mcs mapped character sequence * @param font applicable font * @return glyph position adjustments (or null if no kerning) */ private int[][] getKerningAdjustments(CharSequence mcs, final Font font) { int nc = mcs.length(); // extract kerning array int[] ka = new int[nc]; // kerning array for (int i = 0, n = nc, cPrev = -1; i < n; i++) { int c = mcs.charAt(i); // TODO !BMP if (cPrev >= 0) { ka[i] = font.getKernValue(cPrev, c); } cPrev = c; } // was there a non-zero kerning? boolean hasKerning = false; for (int i = 0, n = nc; i < n; i++) { if (ka[i] != 0) { hasKerning = true; break; } } // if non-zero kerning, then create and return glyph position adjustment array if (hasKerning) { int[][] gpa = new int[nc][4]; for (int i = 0, n = nc; i < n; i++) { if (i > 0) { gpa[i - 1][GlyphPositioningTable.Value.IDX_X_ADVANCE] = ka[i]; } } return gpa; } else { return null; } } private AreaInfo processWordNoMapping(int lastIndex, final Font font, AreaInfo prevAreaInfo, final char breakOpportunityChar, final boolean endsWithHyphen, int level) { boolean kerning = font.hasKerning(); MinOptMax wordIPD = MinOptMax.ZERO; if (LOG.isDebugEnabled()) { LOG.debug("PW: [" + thisStart + "," + lastIndex + "]: {" + " -M" + ", level = " + level + " }"); } for (int i = thisStart; i < lastIndex; i++) { char currentChar = foText.charAt(i); //character width int charWidth = font.getCharWidth(currentChar); wordIPD = wordIPD.plus(charWidth); //kerning if (kerning) { int kern = 0; if (i > thisStart) { char previousChar = foText.charAt(i - 1); kern = font.getKernValue(previousChar, currentChar); } else if (prevAreaInfo != null && !prevAreaInfo.isSpace && prevAreaInfo.breakIndex > 0) { char previousChar = foText.charAt(prevAreaInfo.breakIndex - 1); kern = font.getKernValue(previousChar, currentChar); } if (kern != 0) { addToLetterAdjust(i, kern); wordIPD = wordIPD.plus(kern); } } } if (kerning && (breakOpportunityChar != 0) && !TextLayoutManager.isSpace(breakOpportunityChar) && lastIndex > 0 && endsWithHyphen) { int kern = font.getKernValue(foText.charAt(lastIndex - 1), breakOpportunityChar); if (kern != 0) { addToLetterAdjust(lastIndex, kern); //TODO: add kern to wordIPD? } } // shy+chars at start of word: wordLength == 0 && breakOpportunity // shy only characters in word: wordLength == 0 && !breakOpportunity int wordLength = lastIndex - thisStart; int letterSpaces = 0; if (wordLength != 0) { letterSpaces = wordLength - 1; // if there is a break opportunity and the next one (break character) // is not a space, it could be used as a line end; // add one more letter space, in case other text follows if ((breakOpportunityChar != 0) && !TextLayoutManager.isSpace(breakOpportunityChar)) { letterSpaces++; } } assert letterSpaces >= 0; wordIPD = wordIPD.plus(letterSpaceIPD.mult(letterSpaces)); // create and return the AreaInfo object return new AreaInfo(thisStart, lastIndex, 0, letterSpaces, wordIPD, endsWithHyphen, false, breakOpportunityChar != 0, font, level, null); } private AreaInfo processWord(final int alignment, final KnuthSequence sequence, AreaInfo prevAreaInfo, final char ch, final boolean breakOpportunity, final boolean checkEndsWithHyphen, int level) { //Word boundary found, process widths and kerning int lastIndex = nextStart; while (lastIndex > 0 && foText.charAt(lastIndex - 1) == CharUtilities.SOFT_HYPHEN) { lastIndex--; } final boolean endsWithHyphen = checkEndsWithHyphen && foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN; Font font = FontSelector.selectFontForCharactersInText(foText, thisStart, lastIndex, foText, this); AreaInfo areaInfo; if (font.performsSubstitution() || font.performsPositioning()) { areaInfo = processWordMapping(lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level); } else { areaInfo = processWordNoMapping(lastIndex, font, prevAreaInfo, breakOpportunity ? ch : 0, endsWithHyphen, level); } prevAreaInfo = areaInfo; addAreaInfo(areaInfo); tempStart = nextStart; //add the elements addElementsForAWordFragment(sequence, alignment, areaInfo, areaInfos.size() - 1); thisStart = nextStart; return prevAreaInfo; } /** {@inheritDoc} */ public List addALetterSpaceTo(List oldList) { return addALetterSpaceTo(oldList, 0); } /** {@inheritDoc} */ public List addALetterSpaceTo(final List oldList, int depth) { // old list contains only a box, or the sequence: box penalty glue box; // look at the Position stored in the first element in oldList // which is always a box ListIterator oldListIterator = oldList.listIterator(); KnuthElement knuthElement = (KnuthElement) oldListIterator.next(); Position pos = knuthElement.getPosition(); LeafPosition leafPos = (LeafPosition) pos.getPosition(depth); int index = leafPos.getLeafPos(); //element could refer to '-1' position, for non-collapsed spaces (?) if (index > -1) { AreaInfo areaInfo = getAreaInfo(index); areaInfo.letterSpaceCount++; areaInfo.addToAreaIPD(letterSpaceIPD); if (TextLayoutManager.BREAK_CHARS.indexOf(foText.charAt(tempStart - 1)) >= 0) { // the last character could be used as a line break // append new elements to oldList oldListIterator = oldList.listIterator(oldList.size()); oldListIterator .add(new KnuthPenalty(0, KnuthPenalty.FLAGGED_PENALTY, true, auxiliaryPosition, false)); oldListIterator.add(new KnuthGlue(letterSpaceIPD, auxiliaryPosition, false)); } else if (letterSpaceIPD.isStiff()) { // constant letter space: replace the box // give it the unwrapped position of the replaced element oldListIterator.set(new KnuthInlineBox(areaInfo.areaIPD.getOpt(), alignmentContext, pos, false)); } else { // adjustable letter space: replace the glue oldListIterator.next(); // this would return the penalty element oldListIterator.next(); // this would return the glue element oldListIterator.set( new KnuthGlue(letterSpaceIPD.mult(areaInfo.letterSpaceCount), auxiliaryPosition, true)); } } return oldList; } /** {@inheritDoc} */ public void hyphenate(Position pos, HyphContext hyphContext) { AreaInfo areaInfo = getAreaInfo(((LeafPosition) pos).getLeafPos() + changeOffset); int startIndex = areaInfo.startIndex; int stopIndex; boolean nothingChanged = true; Font font = areaInfo.font; while (startIndex < areaInfo.breakIndex) { MinOptMax newIPD = MinOptMax.ZERO; boolean hyphenFollows; stopIndex = startIndex + hyphContext.getNextHyphPoint(); if (hyphContext.hasMoreHyphPoints() && stopIndex <= areaInfo.breakIndex) { // stopIndex is the index of the first character // after a hyphenation point hyphenFollows = true; } else { // there are no more hyphenation points, // or the next one is after areaInfo.breakIndex hyphenFollows = false; stopIndex = areaInfo.breakIndex; } hyphContext.updateOffset(stopIndex - startIndex); //log.info("Word: " + new String(textArray, startIndex, stopIndex - startIndex)); for (int i = startIndex; i < stopIndex; i++) { char ch = foText.charAt(i); newIPD = newIPD.plus(font.getCharWidth(ch)); //if (i > startIndex) { if (i < stopIndex) { MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1]; if (i == stopIndex - 1 && hyphenFollows) { //the letter adjust here needs to be handled further down during //element generation because it depends on hyph/no-hyph condition letterSpaceAdjust = null; } if (letterSpaceAdjust != null) { newIPD = newIPD.plus(letterSpaceAdjust); } } } // add letter spaces boolean isWordEnd = (stopIndex == areaInfo.breakIndex) && (areaInfo.letterSpaceCount < areaInfo.getWordLength()); int letterSpaceCount = isWordEnd ? stopIndex - startIndex - 1 : stopIndex - startIndex; assert letterSpaceCount >= 0; newIPD = newIPD.plus(letterSpaceIPD.mult(letterSpaceCount)); if (!(nothingChanged && stopIndex == areaInfo.breakIndex && !hyphenFollows)) { // the new AreaInfo object is not equal to the old one changeList .add(new PendingChange( new AreaInfo(startIndex, stopIndex, 0, letterSpaceCount, newIPD, hyphenFollows, false, false, font, -1, null), ((LeafPosition) pos).getLeafPos() + changeOffset)); nothingChanged = false; } startIndex = stopIndex; } hasChanged |= !nothingChanged; } /** {@inheritDoc} */ public boolean applyChanges(final List oldList) { return applyChanges(oldList, 0); } /** {@inheritDoc} */ public boolean applyChanges(final List oldList, int depth) { // make sure the LM appears unfinished in between this call // and the next call to getChangedKnuthElements() setFinished(false); if (oldList.isEmpty()) { return false; } // Find the first and last positions in oldList that point to an AreaInfo // (i.e. getLeafPos() != -1) LeafPosition startPos = null; LeafPosition endPos = null; ListIterator oldListIter; for (oldListIter = oldList.listIterator(); oldListIter.hasNext();) { Position pos = ((KnuthElement) oldListIter.next()).getPosition(); startPos = (LeafPosition) pos.getPosition(depth); if (startPos != null && startPos.getLeafPos() != -1) { break; } } for (oldListIter = oldList.listIterator(oldList.size()); oldListIter.hasPrevious();) { Position pos = ((KnuthElement) oldListIter.previous()).getPosition(); endPos = (LeafPosition) pos.getPosition(depth); if (endPos != null && endPos.getLeafPos() != -1) { break; } } // set start/end index, taking into account any offset due to // changes applied to previous paragraphs returnedIndices[0] = (startPos != null ? startPos.getLeafPos() : -1) + changeOffset; returnedIndices[1] = (endPos != null ? endPos.getLeafPos() : -1) + changeOffset; int areaInfosAdded = 0; int areaInfosRemoved = 0; if (!changeList.isEmpty()) { int oldIndex = -1; int changeIndex; PendingChange currChange; ListIterator changeListIterator = changeList.listIterator(); while (changeListIterator.hasNext()) { currChange = (PendingChange) changeListIterator.next(); if (currChange.index == oldIndex) { areaInfosAdded++; changeIndex = currChange.index + areaInfosAdded - areaInfosRemoved; } else { areaInfosRemoved++; areaInfosAdded++; oldIndex = currChange.index; changeIndex = currChange.index + areaInfosAdded - areaInfosRemoved; removeAreaInfo(changeIndex); } addAreaInfo(changeIndex, currChange.areaInfo); } changeList.clear(); } // increase the end index for getChangedKnuthElements() returnedIndices[1] += (areaInfosAdded - areaInfosRemoved); // increase offset to use for subsequent paragraphs changeOffset += (areaInfosAdded - areaInfosRemoved); return hasChanged; } /** {@inheritDoc} */ public List getChangedKnuthElements(final List oldList, final int alignment) { if (isFinished()) { return null; } final LinkedList returnList = new LinkedList(); for (; returnedIndices[0] <= returnedIndices[1]; returnedIndices[0]++) { AreaInfo areaInfo = getAreaInfo(returnedIndices[0]); if (areaInfo.wordSpaceCount == 0) { // areaInfo refers either to a word or a word fragment addElementsForAWordFragment(returnList, alignment, areaInfo, returnedIndices[0]); } else { // areaInfo refers to a space addElementsForASpace(returnList, alignment, areaInfo, returnedIndices[0]); } } setFinished(returnedIndices[0] == areaInfos.size() - 1); //ElementListObserver.observe(returnList, "text-changed", null); return returnList; } /** {@inheritDoc} */ public String getWordChars(Position pos) { int leafValue = ((LeafPosition) pos).getLeafPos() + changeOffset; if (leafValue != -1) { AreaInfo areaInfo = getAreaInfo(leafValue); StringBuffer buffer = new StringBuffer(areaInfo.getWordLength()); for (int i = areaInfo.startIndex; i < areaInfo.breakIndex; i++) { buffer.append(foText.charAt(i)); } return buffer.toString(); } else { return ""; } } private void addElementsForASpace(List baseList, int alignment, AreaInfo areaInfo, int leafValue) { LeafPosition mainPosition = new LeafPosition(this, leafValue); if (!areaInfo.breakOppAfter) { // a non-breaking space if (alignment == Constants.EN_JUSTIFY) { // the space can stretch and shrink, and must be preserved // when starting a line baseList.add(makeAuxiliaryZeroWidthBox()); baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(areaInfo.areaIPD, mainPosition, false)); } else { // the space does not need to stretch or shrink, and must be // preserved when starting a line baseList.add(new KnuthInlineBox(areaInfo.areaIPD.getOpt(), null, mainPosition, true)); } } else { if (foText.charAt(areaInfo.startIndex) != CharUtilities.SPACE || foText.getWhitespaceTreatment() == Constants.EN_PRESERVE) { // a breaking space that needs to be preserved baseList.addAll(getElementsForBreakingSpace(alignment, areaInfo, auxiliaryPosition, 0, mainPosition, areaInfo.areaIPD.getOpt(), true)); } else { // a (possible block) of breaking spaces baseList.addAll(getElementsForBreakingSpace(alignment, areaInfo, mainPosition, areaInfo.areaIPD.getOpt(), auxiliaryPosition, 0, false)); } } } private List getElementsForBreakingSpace(int alignment, AreaInfo areaInfo, Position pos2, int p2WidthOffset, Position pos3, int p3WidthOffset, boolean skipZeroCheck) { List elements = new ArrayList(); switch (alignment) { case EN_CENTER: // centered text: // if the second element is chosen as a line break these elements // add a constant amount of stretch at the end of a line and at the // beginning of the next one, otherwise they don't add any stretch elements.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); elements.add(makeZeroWidthPenalty(0)); elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP), -6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false)); elements.add(makeAuxiliaryZeroWidthBox()); elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos3, false)); break; case EN_START: // fall through case EN_END: // left- or right-aligned text: // if the second element is chosen as a line break these elements // add a constant amount of stretch at the end of a line, otherwise // they don't add any stretch KnuthGlue g; if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) { g = new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false); elements.add(g); elements.add(makeZeroWidthPenalty(0)); g = new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP), -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false); elements.add(g); elements.add(makeAuxiliaryZeroWidthBox()); elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); g = new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false); elements.add(g); } else { g = new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false); elements.add(g); elements.add(makeZeroWidthPenalty(0)); g = new KnuthGlue(areaInfo.areaIPD.getOpt(), -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false); elements.add(g); } break; case EN_JUSTIFY: // justified text: // the stretch and shrink depends on the space width elements.addAll(getElementsForJustifiedText(areaInfo, pos2, p2WidthOffset, pos3, p3WidthOffset, skipZeroCheck, areaInfo.areaIPD.getShrink())); break; default: // last line justified, the other lines unjustified: // use only the space stretch elements.addAll(getElementsForJustifiedText(areaInfo, pos2, p2WidthOffset, pos3, p3WidthOffset, skipZeroCheck, 0)); } return elements; } private List getElementsForJustifiedText(AreaInfo areaInfo, Position pos2, int p2WidthOffset, Position pos3, int p3WidthOffset, boolean skipZeroCheck, int shrinkability) { int stretchability = areaInfo.areaIPD.getStretch(); List elements = new ArrayList(); if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) { elements.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false)); elements.add(makeZeroWidthPenalty(0)); elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP), stretchability, shrinkability, pos2, false)); elements.add(makeAuxiliaryZeroWidthBox()); elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false)); } else { elements.add(new KnuthGlue(areaInfo.areaIPD.getOpt(), stretchability, shrinkability, pos2, false)); } return elements; } private void addElementsForAWordFragment(List baseList, int alignment, AreaInfo areaInfo, int leafValue) { LeafPosition mainPosition = new LeafPosition(this, leafValue); // if the last character of the word fragment is '-' or '/', // the fragment could end a line; in this case, it loses one // of its letter spaces; boolean suppressibleLetterSpace = areaInfo.breakOppAfter && !areaInfo.isHyphenated; if (letterSpaceIPD.isStiff()) { // constant letter spacing baseList.add( new KnuthInlineBox(suppressibleLetterSpace ? areaInfo.areaIPD.getOpt() - letterSpaceIPD.getOpt() : areaInfo.areaIPD.getOpt(), alignmentContext, notifyPos(mainPosition), false)); } else { // adjustable letter spacing int unsuppressibleLetterSpaces = suppressibleLetterSpace ? areaInfo.letterSpaceCount - 1 : areaInfo.letterSpaceCount; baseList.add(new KnuthInlineBox( areaInfo.areaIPD.getOpt() - areaInfo.letterSpaceCount * letterSpaceIPD.getOpt(), alignmentContext, notifyPos(mainPosition), false)); baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(letterSpaceIPD.mult(unsuppressibleLetterSpaces), auxiliaryPosition, true)); baseList.add(makeAuxiliaryZeroWidthBox()); } // extra-elements if the word fragment is the end of a syllable, // or it ends with a character that can be used as a line break if (areaInfo.isHyphenated) { MinOptMax widthIfNoBreakOccurs = null; if (areaInfo.breakIndex < foText.length()) { //Add in kerning in no-break condition widthIfNoBreakOccurs = letterSpaceAdjustArray[areaInfo.breakIndex]; } //if (areaInfo.breakIndex) // the word fragment ends at the end of a syllable: // if a break occurs the content width increases, // otherwise nothing happens addElementsForAHyphen(baseList, alignment, hyphIPD, widthIfNoBreakOccurs, areaInfo.breakOppAfter && areaInfo.isHyphenated); } else if (suppressibleLetterSpace) { // the word fragment ends with a character that acts as a hyphen // if a break occurs the width does not increase, // otherwise there is one more letter space addElementsForAHyphen(baseList, alignment, 0, letterSpaceIPD, true); } } private void addElementsForAHyphen(List baseList, int alignment, int widthIfBreakOccurs, MinOptMax widthIfNoBreakOccurs, boolean unflagged) { if (widthIfNoBreakOccurs == null) { widthIfNoBreakOccurs = MinOptMax.ZERO; } switch (alignment) { case EN_CENTER: // centered text: baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, true)); baseList.add(new KnuthPenalty(hyphIPD, unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false)); baseList.add(new KnuthGlue(-(lineEndBAP + lineStartBAP), -6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); baseList.add(makeAuxiliaryZeroWidthBox()); baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineStartBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, true)); break; case EN_START: // fall through case EN_END: // left- or right-aligned text: if (lineStartBAP != 0 || lineEndBAP != 0) { baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); baseList.add(new KnuthPenalty(widthIfBreakOccurs, unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false)); baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt() - (lineStartBAP + lineEndBAP), -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); baseList.add(makeAuxiliaryZeroWidthBox()); baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineStartBAP, 0, 0, auxiliaryPosition, false)); } else { baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add( new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); baseList.add(new KnuthPenalty(widthIfBreakOccurs, unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false)); baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt(), -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false)); } break; default: // justified text, or last line justified: // just a flagged penalty if (lineStartBAP != 0 || lineEndBAP != 0) { baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false)); baseList.add(new KnuthPenalty(widthIfBreakOccurs, unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false)); // extra elements representing a letter space that is suppressed // if a break occurs if (widthIfNoBreakOccurs.isNonZero()) { baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt() - (lineStartBAP + lineEndBAP), widthIfNoBreakOccurs.getStretch(), widthIfNoBreakOccurs.getShrink(), auxiliaryPosition, false)); } else { baseList.add(new KnuthGlue(-(lineStartBAP + lineEndBAP), 0, 0, auxiliaryPosition, false)); } baseList.add(makeAuxiliaryZeroWidthBox()); baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE)); baseList.add(new KnuthGlue(lineStartBAP, 0, 0, auxiliaryPosition, false)); } else { baseList.add(new KnuthPenalty(widthIfBreakOccurs, unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false)); // extra elements representing a letter space that is suppressed // if a break occurs if (widthIfNoBreakOccurs.isNonZero()) { baseList.add(new KnuthGlue(widthIfNoBreakOccurs, auxiliaryPosition, false)); } } } } /** {@inheritDoc} */ public String toString() { return super.toString() + "{" + "chars = \'" + CharUtilities.toNCRefs(foText.getCharSequence().toString()) + "\'" + ", len = " + foText.length() + "}"; } }