You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

TextLayoutManager.java 64KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.layoutmgr.inline;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.LinkedList;
  22. import java.util.List;
  23. import java.util.ListIterator;
  24. import org.apache.commons.logging.Log;
  25. import org.apache.commons.logging.LogFactory;
  26. import org.apache.fop.area.Trait;
  27. import org.apache.fop.area.inline.TextArea;
  28. import org.apache.fop.fo.Constants;
  29. import org.apache.fop.fo.FOText;
  30. import org.apache.fop.fonts.Font;
  31. import org.apache.fop.fonts.FontSelector;
  32. import org.apache.fop.fonts.GlyphMapping;
  33. import org.apache.fop.layoutmgr.InlineKnuthSequence;
  34. import org.apache.fop.layoutmgr.KnuthBox;
  35. import org.apache.fop.layoutmgr.KnuthElement;
  36. import org.apache.fop.layoutmgr.KnuthGlue;
  37. import org.apache.fop.layoutmgr.KnuthPenalty;
  38. import org.apache.fop.layoutmgr.KnuthSequence;
  39. import org.apache.fop.layoutmgr.LayoutContext;
  40. import org.apache.fop.layoutmgr.LeafPosition;
  41. import org.apache.fop.layoutmgr.Position;
  42. import org.apache.fop.layoutmgr.PositionIterator;
  43. import org.apache.fop.layoutmgr.TraitSetter;
  44. import org.apache.fop.text.linebreak.LineBreakStatus;
  45. import org.apache.fop.traits.MinOptMax;
  46. import org.apache.fop.traits.SpaceVal;
  47. import org.apache.fop.util.CharUtilities;
  48. import org.apache.fop.util.ListUtil;
  49. /**
  50. * LayoutManager for text (a sequence of characters) which generates one
  51. * or more inline areas.
  52. */
  53. public class TextLayoutManager extends LeafNodeLayoutManager {
  54. //TODO: remove all final modifiers at local variables
  55. // static final int SOFT_HYPHEN_PENALTY = KnuthPenalty.FLAGGED_PENALTY / 10;
  56. private static final int SOFT_HYPHEN_PENALTY = 1;
  57. /**
  58. * this class stores information about changes in vecAreaInfo which are not yet applied
  59. */
  60. private final class PendingChange {
  61. private final GlyphMapping mapping;
  62. private final int index;
  63. private PendingChange(final GlyphMapping mapping, final int index) {
  64. this.mapping = mapping;
  65. this.index = index;
  66. }
  67. }
  68. /**
  69. * logging instance
  70. */
  71. private static final Log LOG = LogFactory.getLog(TextLayoutManager.class);
  72. // Hold all possible breaks for the text in this LM's FO.
  73. private final List<GlyphMapping> mappings;
  74. /** Non-space characters on which we can end a line. */
  75. private static final String BREAK_CHARS = "-/";
  76. private final FOText foText;
  77. /**
  78. * Contains an array of widths to adjust for kerning. The first entry can
  79. * be used to influence the start position of the first letter. The entry i+1 defines the
  80. * cursor advancement after the character i. A null entry means no special advancement.
  81. */
  82. private final MinOptMax[] letterSpaceAdjustArray; //size = textArray.length + 1
  83. /** Font used for the space between words. */
  84. private Font spaceFont;
  85. /** Start index of next TextArea */
  86. private int nextStart;
  87. /** size of a space character (U+0020) glyph in current font */
  88. private int spaceCharIPD;
  89. private MinOptMax wordSpaceIPD;
  90. private MinOptMax letterSpaceIPD;
  91. /** size of the hyphen character glyph in current font */
  92. private int hyphIPD;
  93. private boolean hasChanged;
  94. private int[] returnedIndices = {0, 0};
  95. private int changeOffset;
  96. private int thisStart;
  97. private int tempStart;
  98. private List changeList = new LinkedList();
  99. private AlignmentContext alignmentContext;
  100. /**
  101. * The width to be reserved for border and padding at the start of the line.
  102. */
  103. private int lineStartBAP;
  104. /**
  105. * The width to be reserved for border and padding at the end of the line.
  106. */
  107. private int lineEndBAP;
  108. private boolean keepTogether;
  109. private final Position auxiliaryPosition = new LeafPosition(this, -1);
  110. /**
  111. * Create a Text layout manager.
  112. *
  113. * @param node The FOText object to be rendered
  114. */
  115. public TextLayoutManager(FOText node) {
  116. foText = node;
  117. letterSpaceAdjustArray = new MinOptMax[node.length() + 1];
  118. mappings = new ArrayList<GlyphMapping>();
  119. }
  120. private KnuthPenalty makeZeroWidthPenalty(int penaltyValue) {
  121. return new KnuthPenalty(0, penaltyValue, false, auxiliaryPosition, true);
  122. }
  123. private KnuthBox makeAuxiliaryZeroWidthBox() {
  124. return new KnuthInlineBox(0, null, notifyPos(new LeafPosition(this, -1)), true);
  125. }
  126. /** {@inheritDoc} */
  127. public void initialize() {
  128. foText.resetBuffer();
  129. spaceFont = FontSelector.selectFontForCharacterInText(' ', foText, this);
  130. // With CID fonts, space isn't necessary currentFontState.width(32)
  131. spaceCharIPD = spaceFont.getCharWidth(' ');
  132. // Use hyphenationChar property
  133. // TODO: Use hyphen based on actual font used!
  134. hyphIPD = foText.getCommonHyphenation().getHyphIPD(spaceFont);
  135. SpaceVal letterSpacing = SpaceVal.makeLetterSpacing(foText.getLetterSpacing());
  136. SpaceVal wordSpacing = SpaceVal.makeWordSpacing(foText.getWordSpacing(), letterSpacing,
  137. spaceFont);
  138. // letter space applies only to consecutive non-space characters,
  139. // while word space applies to space characters;
  140. // i.e. the spaces in the string "A SIMPLE TEST" are:
  141. // A<<ws>>S<ls>I<ls>M<ls>P<ls>L<ls>E<<ws>>T<ls>E<ls>S<ls>T
  142. // there is no letter space after the last character of a word,
  143. // nor after a space character
  144. // NOTE: The above is not quite correct. Read on in XSL 1.0, 7.16.2, letter-spacing
  145. // set letter space and word space dimension;
  146. // the default value "normal" was converted into a MinOptMax value
  147. // in the SpaceVal.makeWordSpacing() method
  148. letterSpaceIPD = letterSpacing.getSpace();
  149. wordSpaceIPD = MinOptMax.getInstance(spaceCharIPD).plus(wordSpacing.getSpace());
  150. keepTogether = foText.getKeepTogether().getWithinLine().getEnum() == Constants.EN_ALWAYS;
  151. }
  152. /**
  153. * Generate and add areas to parent area.
  154. * This can either generate an area for each TextArea and each space, or
  155. * an area containing all text with a parameter controlling the size of
  156. * the word space. The latter is most efficient for PDF generation.
  157. * Set size of each area.
  158. * @param posIter Iterator over Position information returned
  159. * by this LayoutManager.
  160. * @param context LayoutContext for adjustments
  161. */
  162. public void addAreas(final PositionIterator posIter, final LayoutContext context) {
  163. // Add word areas
  164. GlyphMapping mapping;
  165. int wordSpaceCount = 0;
  166. int letterSpaceCount = 0;
  167. int firstMappingIndex = -1;
  168. int lastMappingIndex = 0;
  169. MinOptMax realWidth = MinOptMax.ZERO;
  170. /* On first area created, add any leading space.
  171. * Calculate word-space stretch value.
  172. */
  173. GlyphMapping lastMapping = null;
  174. while (posIter.hasNext()) {
  175. Position nextPos = posIter.next();
  176. assert (nextPos instanceof LeafPosition);
  177. final LeafPosition tbpNext = (LeafPosition) nextPos;
  178. if (tbpNext == null) {
  179. continue; //Ignore elements without Positions
  180. }
  181. if (tbpNext.getLeafPos() != -1) {
  182. mapping = mappings.get(tbpNext.getLeafPos());
  183. if (lastMapping == null
  184. || (mapping.font != lastMapping.font)
  185. || (mapping.level != lastMapping.level)) {
  186. if (lastMapping != null) {
  187. addMappingAreas(lastMapping, wordSpaceCount,
  188. letterSpaceCount, firstMappingIndex,
  189. lastMappingIndex, realWidth, context);
  190. }
  191. firstMappingIndex = tbpNext.getLeafPos();
  192. wordSpaceCount = 0;
  193. letterSpaceCount = 0;
  194. realWidth = MinOptMax.ZERO;
  195. }
  196. wordSpaceCount += mapping.wordSpaceCount;
  197. letterSpaceCount += mapping.letterSpaceCount;
  198. realWidth = realWidth.plus(mapping.areaIPD);
  199. lastMappingIndex = tbpNext.getLeafPos();
  200. lastMapping = mapping;
  201. }
  202. }
  203. if (lastMapping != null) {
  204. addMappingAreas(lastMapping, wordSpaceCount, letterSpaceCount, firstMappingIndex,
  205. lastMappingIndex, realWidth, context);
  206. }
  207. }
  208. private void addMappingAreas(GlyphMapping mapping, int wordSpaceCount, int letterSpaceCount,
  209. int firstMappingIndex, int lastMappingIndex,
  210. MinOptMax realWidth, LayoutContext context) {
  211. // TODO: These two statements (if, for) were like this before my recent
  212. // changes. However, it seems as if they should use the GlyphMapping from
  213. // firstMappingIndex.. lastMappingIndex rather than just the last mapping.
  214. // This needs to be checked.
  215. int textLength = mapping.getWordLength();
  216. if (mapping.letterSpaceCount == textLength && !mapping.isHyphenated
  217. && context.isLastArea()) {
  218. // the line ends at a character like "/" or "-";
  219. // remove the letter space after the last character
  220. realWidth = realWidth.minus(letterSpaceIPD);
  221. letterSpaceCount--;
  222. }
  223. for (int i = mapping.startIndex; i < mapping.endIndex; i++) {
  224. MinOptMax letterSpaceAdjustment = letterSpaceAdjustArray[i + 1];
  225. if (letterSpaceAdjustment != null && letterSpaceAdjustment.isElastic()) {
  226. letterSpaceCount++;
  227. }
  228. }
  229. // add hyphenation character if the last word is hyphenated
  230. if (context.isLastArea() && mapping.isHyphenated) {
  231. realWidth = realWidth.plus(hyphIPD);
  232. }
  233. /* Calculate adjustments */
  234. double ipdAdjust = context.getIPDAdjust();
  235. // calculate total difference between real and available width
  236. int difference;
  237. if (ipdAdjust > 0.0) {
  238. difference = (int) (realWidth.getStretch() * ipdAdjust);
  239. } else {
  240. difference = (int) (realWidth.getShrink() * ipdAdjust);
  241. }
  242. // set letter space adjustment
  243. int letterSpaceDim = letterSpaceIPD.getOpt();
  244. if (ipdAdjust > 0.0) {
  245. letterSpaceDim += (int) (letterSpaceIPD.getStretch() * ipdAdjust);
  246. } else {
  247. letterSpaceDim += (int) (letterSpaceIPD.getShrink() * ipdAdjust);
  248. }
  249. int totalAdjust = (letterSpaceDim - letterSpaceIPD.getOpt()) * letterSpaceCount;
  250. // set word space adjustment
  251. int wordSpaceDim = wordSpaceIPD.getOpt();
  252. if (wordSpaceCount > 0) {
  253. wordSpaceDim += (difference - totalAdjust) / wordSpaceCount;
  254. }
  255. totalAdjust += (wordSpaceDim - wordSpaceIPD.getOpt()) * wordSpaceCount;
  256. if (totalAdjust != difference) {
  257. // the applied adjustment is greater or smaller than the needed one
  258. TextLayoutManager.LOG
  259. .trace("TextLM.addAreas: error in word / letter space adjustment = "
  260. + (totalAdjust - difference));
  261. // set totalAdjust = difference, so that the width of the TextArea
  262. // will counterbalance the error and the other inline areas will be
  263. // placed correctly
  264. totalAdjust = difference;
  265. }
  266. TextArea textArea = new TextAreaBuilder(realWidth, totalAdjust, context, firstMappingIndex,
  267. lastMappingIndex, context.isLastArea(), mapping.font).build();
  268. // wordSpaceDim is computed in relation to wordSpaceIPD.opt
  269. // but the renderer needs to know the adjustment in relation
  270. // to the size of the space character in the current font;
  271. // moreover, the pdf renderer adds the character spacing even to
  272. // the last character of a word and to space characters: in order
  273. // to avoid this, we must subtract the letter space width twice;
  274. // the renderer will compute the space width as:
  275. // space width =
  276. // = "normal" space width + letterSpaceAdjust + wordSpaceAdjust
  277. // = spaceCharIPD + letterSpaceAdjust +
  278. // + (wordSpaceDim - spaceCharIPD - 2 * letterSpaceAdjust)
  279. // = wordSpaceDim - letterSpaceAdjust
  280. textArea.setTextLetterSpaceAdjust(letterSpaceDim);
  281. textArea.setTextWordSpaceAdjust(wordSpaceDim - spaceCharIPD
  282. - 2 * textArea.getTextLetterSpaceAdjust());
  283. if (context.getIPDAdjust() != 0) {
  284. // add information about space width
  285. textArea.setSpaceDifference(wordSpaceIPD.getOpt() - spaceCharIPD
  286. - 2 * textArea.getTextLetterSpaceAdjust());
  287. }
  288. parentLayoutManager.addChildArea(textArea);
  289. }
  290. private final class TextAreaBuilder {
  291. // constructor initialized state
  292. private final MinOptMax width; // content ipd
  293. private final int adjust; // content ipd adjustment
  294. private final LayoutContext context; // layout context
  295. private final int firstIndex; // index of first GlyphMapping
  296. private final int lastIndex; // index of last GlyphMapping
  297. private final boolean isLastArea; // true if last inline area in line area
  298. private final Font font; // applicable font
  299. // other, non-constructor state
  300. private TextArea textArea; // text area being constructed
  301. private int blockProgressionDimension; // calculated bpd
  302. private GlyphMapping mapping; // current mapping when iterating over words
  303. private StringBuffer wordChars; // current word's character buffer
  304. private int[] letterSpaceAdjust; // current word's letter space adjustments
  305. private int letterSpaceAdjustIndex; // last written letter space adjustment index
  306. private int[] wordLevels; // current word's bidi levels
  307. private int wordLevelsIndex; // last written bidi level index
  308. private int wordIPD; // accumulated ipd of current word
  309. private int[][] gposAdjustments; // current word's glyph position adjustments
  310. private int gposAdjustmentsIndex; // last written glyph position adjustment index
  311. /**
  312. * Creates a new <code>TextAreaBuilder</code> which itself builds an inline word area. This
  313. * creates a TextArea and sets up the various attributes.
  314. *
  315. * @param width the MinOptMax width of the content
  316. * @param adjust the total ipd adjustment with respect to the optimal width
  317. * @param context the layout context
  318. * @param firstIndex the index of the first GlyphMapping used for the TextArea
  319. * @param lastIndex the index of the last GlyphMapping used for the TextArea
  320. * @param isLastArea is this TextArea the last in a line?
  321. * @param font Font to be used in this particular TextArea
  322. */
  323. private TextAreaBuilder(MinOptMax width, int adjust, LayoutContext context,
  324. int firstIndex, int lastIndex, boolean isLastArea, Font font) {
  325. this.width = width;
  326. this.adjust = adjust;
  327. this.context = context;
  328. this.firstIndex = firstIndex;
  329. this.lastIndex = lastIndex;
  330. this.isLastArea = isLastArea;
  331. this.font = font;
  332. }
  333. private TextArea build() {
  334. createTextArea();
  335. setInlineProgressionDimension();
  336. calcBlockProgressionDimension();
  337. setBlockProgressionDimension();
  338. setBaselineOffset();
  339. setBlockProgressionOffset();
  340. setText();
  341. TraitSetter.addFontTraits(textArea, font);
  342. textArea.addTrait(Trait.COLOR, foText.getColor());
  343. TraitSetter.addTextDecoration(textArea, foText.getTextDecoration());
  344. if (!context.treatAsArtifact()) {
  345. TraitSetter.addStructureTreeElement(textArea, foText.getStructureTreeElement());
  346. }
  347. return textArea;
  348. }
  349. /**
  350. * Creates an plain <code>TextArea</code> or a justified <code>TextArea</code> with
  351. * additional information.
  352. */
  353. private void createTextArea() {
  354. if (context.getIPDAdjust() == 0.0) {
  355. textArea = new TextArea();
  356. } else {
  357. textArea = new TextArea(width.getStretch(), width.getShrink(),
  358. adjust);
  359. }
  360. }
  361. private void setInlineProgressionDimension() {
  362. textArea.setIPD(width.getOpt() + adjust);
  363. }
  364. private void calcBlockProgressionDimension() {
  365. blockProgressionDimension = font.getAscender() - font.getDescender();
  366. }
  367. private void setBlockProgressionDimension() {
  368. textArea.setBPD(blockProgressionDimension);
  369. }
  370. private void setBaselineOffset() {
  371. textArea.setBaselineOffset(font.getAscender());
  372. }
  373. private void setBlockProgressionOffset() {
  374. if (blockProgressionDimension == alignmentContext.getHeight()) {
  375. textArea.setBlockProgressionOffset(0);
  376. } else {
  377. textArea.setBlockProgressionOffset(alignmentContext.getOffset());
  378. }
  379. }
  380. /**
  381. * Sets the text of the TextArea, split into words and spaces.
  382. */
  383. private void setText() {
  384. int mappingIndex = -1;
  385. int wordCharLength = 0;
  386. for (int wordIndex = firstIndex; wordIndex <= lastIndex; wordIndex++) {
  387. mapping = getGlyphMapping(wordIndex);
  388. textArea.updateLevel(mapping.level);
  389. if (mapping.isSpace) {
  390. addSpaces();
  391. } else {
  392. // mapping stores information about a word fragment
  393. if (mappingIndex == -1) {
  394. // here starts a new word
  395. mappingIndex = wordIndex;
  396. wordCharLength = 0;
  397. }
  398. wordCharLength += mapping.getWordLength();
  399. if (isWordEnd(wordIndex)) {
  400. addWord(mappingIndex, wordIndex, wordCharLength);
  401. mappingIndex = -1;
  402. }
  403. }
  404. }
  405. }
  406. private boolean isWordEnd(int mappingIndex) {
  407. return mappingIndex == lastIndex || getGlyphMapping(mappingIndex + 1).isSpace;
  408. }
  409. /**
  410. * Add word with fragments from STARTINDEX to ENDINDEX, where
  411. * total length of (possibly mapped) word is CHARLENGTH.
  412. * A word is composed from one or more word fragments, where each
  413. * fragment corresponds to distinct instance in a sequence of
  414. * area info instances starting at STARTINDEX continuing through (and
  415. * including) ENDINDEX.
  416. * @param startIndex index of first area info of word to add
  417. * @param endIndex index of last area info of word to add
  418. * @param wordLength number of (mapped) characters in word
  419. */
  420. private void addWord(int startIndex, int endIndex, int wordLength) {
  421. int blockProgressionOffset = 0;
  422. boolean gposAdjusted = false;
  423. if (isHyphenated(endIndex)) {
  424. // TODO may be problematic in some I18N contexts [GA]
  425. wordLength++;
  426. }
  427. initWord(wordLength);
  428. // iterate over word's fragments, adding word chars (with bidi
  429. // levels), letter space adjustments, and glyph position adjustments
  430. for (int i = startIndex; i <= endIndex; i++) {
  431. GlyphMapping wordMapping = getGlyphMapping(i);
  432. addWordChars(wordMapping);
  433. addLetterAdjust(wordMapping);
  434. if (addGlyphPositionAdjustments(wordMapping)) {
  435. gposAdjusted = true;
  436. }
  437. }
  438. if (isHyphenated(endIndex)) {
  439. // TODO may be problematic in some I18N contexts [GA]
  440. addHyphenationChar();
  441. }
  442. if (!gposAdjusted) {
  443. gposAdjustments = null;
  444. }
  445. textArea.addWord(wordChars.toString(), wordIPD, letterSpaceAdjust,
  446. getNonEmptyLevels(), gposAdjustments, blockProgressionOffset);
  447. }
  448. private int[] getNonEmptyLevels() {
  449. if (wordLevels != null) {
  450. assert wordLevelsIndex <= wordLevels.length;
  451. boolean empty = true;
  452. for (int i = 0, n = wordLevelsIndex; i < n; i++) {
  453. if (wordLevels [ i ] >= 0) {
  454. empty = false;
  455. break;
  456. }
  457. }
  458. return empty ? null : wordLevels;
  459. } else {
  460. return null;
  461. }
  462. }
  463. /**
  464. * Fully allocate word character buffer, letter space adjustments
  465. * array, bidi levels array, and glyph position adjustments array.
  466. * based on full word length, including all (possibly mapped) fragments.
  467. * @param wordLength length of word including all (possibly mapped) fragments
  468. */
  469. private void initWord(int wordLength) {
  470. wordChars = new StringBuffer(wordLength);
  471. letterSpaceAdjust = new int[wordLength];
  472. letterSpaceAdjustIndex = 0;
  473. wordLevels = new int[wordLength];
  474. wordLevelsIndex = 0;
  475. Arrays.fill(wordLevels, -1);
  476. gposAdjustments = new int[wordLength][4];
  477. gposAdjustmentsIndex = 0;
  478. wordIPD = 0;
  479. }
  480. private boolean isHyphenated(int endIndex) {
  481. return isLastArea && endIndex == lastIndex && mapping.isHyphenated;
  482. }
  483. private void addHyphenationChar() {
  484. wordChars.append(foText.getCommonHyphenation().getHyphChar(font));
  485. // [TBD] expand bidi word levels, letter space adjusts, gpos adjusts
  486. // [TBD] [GA] problematic in bidi context... what is level of hyphen?
  487. textArea.setHyphenated();
  488. }
  489. /**
  490. * Given a word area info associated with a word fragment,
  491. * (1) concatenate (possibly mapped) word characters to word character buffer;
  492. * (2) concatenante (possibly mapped) word bidi levels to levels buffer;
  493. * (3) update word's IPD with optimal IPD of fragment.
  494. * @param wordMapping fragment info
  495. */
  496. private void addWordChars(GlyphMapping wordMapping) {
  497. int s = wordMapping.startIndex;
  498. int e = wordMapping.endIndex;
  499. if (wordMapping.mapping != null) {
  500. wordChars.append(wordMapping.mapping);
  501. addWordLevels(getMappingBidiLevels(wordMapping));
  502. } else {
  503. for (int i = s; i < e; i++) {
  504. wordChars.append(foText.charAt(i));
  505. }
  506. addWordLevels(foText.getBidiLevels(s, e));
  507. }
  508. wordIPD += wordMapping.areaIPD.getOpt();
  509. }
  510. /**
  511. * Obtain bidirectional levels of mapping of characters over specific interval.
  512. * @param start index in character buffer
  513. * @param end index in character buffer
  514. * @return a (possibly empty) array of bidi levels or null
  515. * in case no bidi levels have been assigned
  516. */
  517. private int[] getMappingBidiLevels(GlyphMapping mapping) {
  518. if (mapping.mapping != null) {
  519. int nc = mapping.endIndex - mapping.startIndex;
  520. int nm = mapping.mapping.length();
  521. int[] la = foText.getBidiLevels(mapping.startIndex, mapping.endIndex);
  522. if (la == null) {
  523. return null;
  524. } else if (nm == nc) { // mapping is same length as mapped range
  525. return la;
  526. } else if (nm > nc) { // mapping is longer than mapped range
  527. int[] ma = new int[nm];
  528. System.arraycopy(la, 0, ma, 0, la.length);
  529. for (int i = la.length, n = ma.length, l = (i > 0) ? la[i - 1] : 0; i < n; i++) {
  530. ma[i] = l;
  531. }
  532. return ma;
  533. } else { // mapping is shorter than mapped range
  534. int[] ma = new int[nm];
  535. System.arraycopy(la, 0, ma, 0, ma.length);
  536. return ma;
  537. }
  538. } else {
  539. return foText.getBidiLevels(mapping.startIndex, mapping.endIndex);
  540. }
  541. }
  542. /**
  543. * Given a (possibly null) bidi levels array associated with a word fragment,
  544. * concatenante (possibly mapped) word bidi levels to levels buffer.
  545. * @param levels bidi levels array or null
  546. */
  547. private void addWordLevels(int[] levels) {
  548. int numLevels = (levels != null) ? levels.length : 0;
  549. if (numLevels > 0) {
  550. int need = wordLevelsIndex + numLevels;
  551. if (need <= wordLevels.length) {
  552. System.arraycopy(levels, 0, wordLevels, wordLevelsIndex, numLevels);
  553. } else {
  554. throw new IllegalStateException(
  555. "word levels array too short: expect at least "
  556. + need + " entries, but has only " + wordLevels.length + " entries");
  557. }
  558. }
  559. wordLevelsIndex += numLevels;
  560. }
  561. /**
  562. * Given a word area info associated with a word fragment,
  563. * concatenate letter space adjustments for each (possibly mapped) character.
  564. * @param wordMapping fragment info
  565. */
  566. private void addLetterAdjust(GlyphMapping wordMapping) {
  567. int letterSpaceCount = wordMapping.letterSpaceCount;
  568. int wordLength = wordMapping.getWordLength();
  569. int taAdjust = textArea.getTextLetterSpaceAdjust();
  570. for (int i = 0, n = wordLength; i < n; i++) {
  571. int j = letterSpaceAdjustIndex + i;
  572. if (j > 0) {
  573. int k = wordMapping.startIndex + i;
  574. MinOptMax adj = (k < letterSpaceAdjustArray.length)
  575. ? letterSpaceAdjustArray [ k ] : null;
  576. letterSpaceAdjust [ j ] = (adj == null) ? 0 : adj.getOpt();
  577. }
  578. if (letterSpaceCount > 0) {
  579. letterSpaceAdjust [ j ] += taAdjust;
  580. letterSpaceCount--;
  581. }
  582. }
  583. letterSpaceAdjustIndex += wordLength;
  584. }
  585. /**
  586. * Given a word area info associated with a word fragment,
  587. * concatenate glyph position adjustments for each (possibly mapped) character.
  588. * @param wordMapping fragment info
  589. * @return true if an adjustment was non-zero
  590. */
  591. private boolean addGlyphPositionAdjustments(GlyphMapping wordMapping) {
  592. boolean adjusted = false;
  593. int[][] gpa = wordMapping.gposAdjustments;
  594. int numAdjusts = (gpa != null) ? gpa.length : 0;
  595. int wordLength = wordMapping.getWordLength();
  596. if (numAdjusts > 0) {
  597. int need = gposAdjustmentsIndex + numAdjusts;
  598. if (need <= gposAdjustments.length) {
  599. for (int i = 0, n = wordLength, j = 0; i < n; i++) {
  600. if (i < numAdjusts) {
  601. int[] wpa1 = gposAdjustments [ gposAdjustmentsIndex + i ];
  602. int[] wpa2 = gpa [ j++ ];
  603. for (int k = 0; k < 4; k++) {
  604. int a = wpa2 [ k ];
  605. if (a != 0) {
  606. wpa1 [ k ] += a;
  607. adjusted = true;
  608. }
  609. }
  610. }
  611. }
  612. } else {
  613. throw new IllegalStateException(
  614. "gpos adjustments array too short: expect at least "
  615. + need + " entries, but has only " + gposAdjustments.length
  616. + " entries");
  617. }
  618. }
  619. gposAdjustmentsIndex += wordLength;
  620. return adjusted;
  621. }
  622. /**
  623. * The <code>GlyphMapping</code> stores information about spaces.
  624. * <p>
  625. * Add the spaces - except zero-width spaces - to the TextArea.
  626. */
  627. private void addSpaces() {
  628. int blockProgressionOffset = 0;
  629. // [TBD] need to better handling of spaceIPD assignment, for now,
  630. // divide the area info's allocated IPD evenly among the
  631. // non-zero-width space characters
  632. int numZeroWidthSpaces = 0;
  633. for (int i = mapping.startIndex; i < mapping.endIndex; i++) {
  634. char spaceChar = foText.charAt(i);
  635. if (CharUtilities.isZeroWidthSpace(spaceChar)) {
  636. numZeroWidthSpaces++;
  637. }
  638. }
  639. int numSpaces = mapping.endIndex - mapping.startIndex - numZeroWidthSpaces;
  640. int spaceIPD = mapping.areaIPD.getOpt() / ((numSpaces > 0) ? numSpaces : 1);
  641. // add space area children, one for each non-zero-width space character
  642. for (int i = mapping.startIndex; i < mapping.endIndex; i++) {
  643. char spaceChar = foText.charAt(i);
  644. int level = foText.bidiLevelAt(i);
  645. if (!CharUtilities.isZeroWidthSpace(spaceChar)) {
  646. textArea.addSpace(
  647. spaceChar, spaceIPD,
  648. CharUtilities.isAdjustableSpace(spaceChar),
  649. blockProgressionOffset, level);
  650. }
  651. }
  652. }
  653. }
  654. private void addGlyphMapping(GlyphMapping mapping) {
  655. addGlyphMapping(mappings.size(), mapping);
  656. }
  657. private void addGlyphMapping(int index, GlyphMapping mapping) {
  658. mappings.add(index, mapping);
  659. }
  660. private void removeGlyphMapping(int index) {
  661. mappings.remove(index);
  662. }
  663. private GlyphMapping getGlyphMapping(int index) {
  664. return mappings.get(index);
  665. }
  666. /** {@inheritDoc} */
  667. public List getNextKnuthElements(final LayoutContext context, final int alignment) {
  668. lineStartBAP = context.getLineStartBorderAndPaddingWidth();
  669. lineEndBAP = context.getLineEndBorderAndPaddingWidth();
  670. alignmentContext = context.getAlignmentContext();
  671. final List returnList = new LinkedList();
  672. KnuthSequence sequence = new InlineKnuthSequence();
  673. GlyphMapping mapping = null;
  674. GlyphMapping prevMapping = null;
  675. returnList.add(sequence);
  676. if (LOG.isDebugEnabled()) {
  677. LOG.debug("GK: [" + nextStart + "," + foText.length() + "]");
  678. }
  679. LineBreakStatus lineBreakStatus = new LineBreakStatus();
  680. thisStart = nextStart;
  681. boolean inWord = false;
  682. boolean inWhitespace = false;
  683. char ch = 0;
  684. int level = -1;
  685. int prevLevel = -1;
  686. boolean retainControls = false;
  687. while (nextStart < foText.length()) {
  688. ch = foText.charAt(nextStart);
  689. level = foText.bidiLevelAt(nextStart);
  690. boolean breakOpportunity = false;
  691. byte breakAction = keepTogether
  692. ? LineBreakStatus.PROHIBITED_BREAK
  693. : lineBreakStatus.nextChar(ch);
  694. switch (breakAction) {
  695. case LineBreakStatus.COMBINING_PROHIBITED_BREAK:
  696. case LineBreakStatus.PROHIBITED_BREAK:
  697. break;
  698. case LineBreakStatus.EXPLICIT_BREAK:
  699. break;
  700. case LineBreakStatus.COMBINING_INDIRECT_BREAK:
  701. case LineBreakStatus.DIRECT_BREAK:
  702. case LineBreakStatus.INDIRECT_BREAK:
  703. breakOpportunity = true;
  704. break;
  705. default:
  706. TextLayoutManager.LOG.error("Unexpected breakAction: " + breakAction);
  707. }
  708. if (LOG.isDebugEnabled()) {
  709. LOG.debug("GK: {"
  710. + " index = " + nextStart
  711. + ", char = " + CharUtilities.charToNCRef(ch)
  712. + ", level = " + level
  713. + ", levelPrev = " + prevLevel
  714. + ", inWord = " + inWord
  715. + ", inSpace = " + inWhitespace
  716. + "}");
  717. }
  718. if (inWord) {
  719. if (breakOpportunity
  720. || GlyphMapping.isSpace(ch)
  721. || CharUtilities.isExplicitBreak(ch)
  722. || ((prevLevel != -1) && (level != prevLevel))) {
  723. // this.foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN
  724. prevMapping = processWord(alignment, sequence, prevMapping, ch,
  725. breakOpportunity, true, prevLevel, retainControls);
  726. }
  727. } else if (inWhitespace) {
  728. if (ch != CharUtilities.SPACE || breakOpportunity) {
  729. prevMapping = processWhitespace(alignment, sequence,
  730. breakOpportunity, prevLevel);
  731. }
  732. } else {
  733. if (mapping != null) {
  734. prevMapping = mapping;
  735. processLeftoverGlyphMapping(alignment, sequence, mapping,
  736. ch == CharUtilities.SPACE || breakOpportunity);
  737. mapping = null;
  738. }
  739. if (breakAction == LineBreakStatus.EXPLICIT_BREAK) {
  740. sequence = processLinebreak(returnList, sequence);
  741. }
  742. }
  743. if (ch == CharUtilities.SPACE
  744. && foText.getWhitespaceTreatment() == Constants.EN_PRESERVE
  745. || ch == CharUtilities.NBSPACE) {
  746. final Font font = FontSelector.selectFontForCharacterInText(ch,
  747. this.foText, this);
  748. font.mapChar(ch);
  749. // preserved space or non-breaking space:
  750. // create the GlyphMapping object
  751. mapping = new GlyphMapping(nextStart, nextStart + 1, 1, 0, wordSpaceIPD, false, true,
  752. breakOpportunity, spaceFont, level, null);
  753. thisStart = nextStart + 1;
  754. } else if (CharUtilities.isFixedWidthSpace(ch) || CharUtilities.isZeroWidthSpace(ch)) {
  755. // create the GlyphMapping object
  756. Font font = FontSelector.selectFontForCharacterInText(ch, foText, this);
  757. MinOptMax ipd = MinOptMax.getInstance(font.getCharWidth(ch));
  758. mapping = new GlyphMapping(nextStart, nextStart + 1, 0, 0, ipd, false, true,
  759. breakOpportunity, font, level, null);
  760. thisStart = nextStart + 1;
  761. } else if (CharUtilities.isExplicitBreak(ch)) {
  762. //mandatory break-character: only advance index
  763. thisStart = nextStart + 1;
  764. }
  765. inWord = !GlyphMapping.isSpace(ch) && !CharUtilities.isExplicitBreak(ch);
  766. inWhitespace = ch == CharUtilities.SPACE
  767. && foText.getWhitespaceTreatment() != Constants.EN_PRESERVE;
  768. prevLevel = level;
  769. nextStart++;
  770. }
  771. // Process any last elements
  772. if (inWord) {
  773. processWord(alignment, sequence, prevMapping, ch, false, false, prevLevel, retainControls);
  774. } else if (inWhitespace) {
  775. processWhitespace(alignment, sequence, !keepTogether, prevLevel);
  776. } else if (mapping != null) {
  777. processLeftoverGlyphMapping(alignment, sequence, mapping,
  778. ch == CharUtilities.ZERO_WIDTH_SPACE);
  779. } else if (CharUtilities.isExplicitBreak(ch)) {
  780. this.processLinebreak(returnList, sequence);
  781. }
  782. if (((List) ListUtil.getLast(returnList)).isEmpty()) {
  783. //Remove an empty sequence because of a trailing newline
  784. ListUtil.removeLast(returnList);
  785. }
  786. setFinished(true);
  787. if (returnList.isEmpty()) {
  788. return null;
  789. } else {
  790. return returnList;
  791. }
  792. }
  793. private KnuthSequence processLinebreak(List returnList, KnuthSequence sequence) {
  794. if (lineEndBAP != 0) {
  795. sequence.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, true));
  796. }
  797. sequence.endSequence();
  798. sequence = new InlineKnuthSequence();
  799. returnList.add(sequence);
  800. return sequence;
  801. }
  802. private void processLeftoverGlyphMapping(int alignment, KnuthSequence sequence,
  803. GlyphMapping mapping, boolean breakOpportunityAfter) {
  804. addGlyphMapping(mapping);
  805. mapping.breakOppAfter = breakOpportunityAfter;
  806. addElementsForASpace(sequence, alignment, mapping, mappings.size() - 1);
  807. }
  808. private GlyphMapping processWhitespace(final int alignment,
  809. final KnuthSequence sequence, final boolean breakOpportunity, int level) {
  810. if (LOG.isDebugEnabled()) {
  811. LOG.debug("PS: [" + thisStart + "," + nextStart + "]");
  812. }
  813. // End of whitespace
  814. // create the GlyphMapping object
  815. assert nextStart >= thisStart;
  816. GlyphMapping mapping = new GlyphMapping(
  817. thisStart, nextStart, nextStart - thisStart, 0,
  818. wordSpaceIPD.mult(nextStart - thisStart),
  819. false, true, breakOpportunity, spaceFont, level, null);
  820. addGlyphMapping(mapping);
  821. // create the elements
  822. addElementsForASpace(sequence, alignment, mapping, mappings.size() - 1);
  823. thisStart = nextStart;
  824. return mapping;
  825. }
  826. private GlyphMapping processWord(final int alignment, final KnuthSequence sequence,
  827. GlyphMapping prevMapping, final char ch, final boolean breakOpportunity,
  828. final boolean checkEndsWithHyphen, int level, boolean retainControls) {
  829. //Word boundary found, process widths and kerning
  830. int lastIndex = nextStart;
  831. while (lastIndex > 0 && foText.charAt(lastIndex - 1) == CharUtilities.SOFT_HYPHEN) {
  832. lastIndex--;
  833. }
  834. final boolean endsWithHyphen = checkEndsWithHyphen
  835. && foText.charAt(lastIndex) == CharUtilities.SOFT_HYPHEN;
  836. Font font = FontSelector.selectFontForCharactersInText(
  837. foText, thisStart, lastIndex, foText, this);
  838. char breakOpportunityChar = breakOpportunity ? ch : 0;
  839. char precedingChar = prevMapping != null && !prevMapping.isSpace
  840. && prevMapping.endIndex > 0 ? foText.charAt(prevMapping.endIndex - 1) : 0;
  841. GlyphMapping mapping = GlyphMapping.doGlyphMapping(foText, thisStart, lastIndex, font,
  842. letterSpaceIPD, letterSpaceAdjustArray, precedingChar, breakOpportunityChar,
  843. endsWithHyphen, level, false, false, retainControls);
  844. prevMapping = mapping;
  845. addGlyphMapping(mapping);
  846. tempStart = nextStart;
  847. //add the elements
  848. addElementsForAWordFragment(sequence, alignment, mapping, mappings.size() - 1);
  849. thisStart = nextStart;
  850. return prevMapping;
  851. }
  852. /** {@inheritDoc} */
  853. public List addALetterSpaceTo(List oldList) {
  854. return addALetterSpaceTo(oldList, 0);
  855. }
  856. /** {@inheritDoc} */
  857. public List addALetterSpaceTo(final List oldList, int depth) {
  858. // old list contains only a box, or the sequence: box penalty glue box;
  859. // look at the Position stored in the first element in oldList
  860. // which is always a box
  861. ListIterator oldListIterator = oldList.listIterator();
  862. KnuthElement knuthElement = (KnuthElement) oldListIterator.next();
  863. Position pos = knuthElement.getPosition();
  864. Position innerPosition = pos.getPosition(depth);
  865. assert (innerPosition instanceof LeafPosition);
  866. LeafPosition leafPos = (LeafPosition) innerPosition;
  867. int index = leafPos.getLeafPos();
  868. //element could refer to '-1' position, for non-collapsed spaces (?)
  869. if (index > -1) {
  870. GlyphMapping mapping = getGlyphMapping(index);
  871. mapping.letterSpaceCount++;
  872. mapping.addToAreaIPD(letterSpaceIPD);
  873. if (TextLayoutManager.BREAK_CHARS.indexOf(foText.charAt(tempStart - 1)) >= 0) {
  874. // the last character could be used as a line break
  875. // append new elements to oldList
  876. oldListIterator = oldList.listIterator(oldList.size());
  877. oldListIterator.add(new KnuthPenalty(0, KnuthPenalty.FLAGGED_PENALTY, true,
  878. auxiliaryPosition, false));
  879. oldListIterator.add(new KnuthGlue(letterSpaceIPD, auxiliaryPosition, false));
  880. } else if (letterSpaceIPD.isStiff()) {
  881. // constant letter space: replace the box
  882. // give it the unwrapped position of the replaced element
  883. oldListIterator.set(new KnuthInlineBox(mapping.areaIPD.getOpt(),
  884. alignmentContext, pos, false));
  885. } else {
  886. // adjustable letter space: replace the glue
  887. oldListIterator.next(); // this would return the penalty element
  888. oldListIterator.next(); // this would return the glue element
  889. oldListIterator.set(new KnuthGlue(letterSpaceIPD.mult(mapping.letterSpaceCount),
  890. auxiliaryPosition, true));
  891. }
  892. }
  893. return oldList;
  894. }
  895. /** {@inheritDoc} */
  896. public void hyphenate(Position pos, HyphContext hyphContext) {
  897. GlyphMapping mapping = getGlyphMapping(((LeafPosition) pos).getLeafPos() + changeOffset);
  898. int startIndex = mapping.startIndex;
  899. int stopIndex;
  900. boolean nothingChanged = true;
  901. Font font = mapping.font;
  902. while (startIndex < mapping.endIndex) {
  903. MinOptMax newIPD = MinOptMax.ZERO;
  904. boolean hyphenFollows;
  905. stopIndex = startIndex + hyphContext.getNextHyphPoint();
  906. if (hyphContext.hasMoreHyphPoints() && stopIndex <= mapping.endIndex) {
  907. // stopIndex is the index of the first character
  908. // after a hyphenation point
  909. hyphenFollows = true;
  910. } else {
  911. // there are no more hyphenation points,
  912. // or the next one is after mapping.breakIndex
  913. hyphenFollows = false;
  914. stopIndex = mapping.endIndex;
  915. }
  916. hyphContext.updateOffset(stopIndex - startIndex);
  917. //log.info("Word: " + new String(textArray, startIndex, stopIndex - startIndex));
  918. for (int i = startIndex; i < stopIndex; i++) {
  919. int cp = Character.codePointAt(foText, i);
  920. i += Character.charCount(cp) - 1;
  921. newIPD = newIPD.plus(font.getCharWidth(cp));
  922. //if (i > startIndex) {
  923. if (i < stopIndex) {
  924. MinOptMax letterSpaceAdjust = letterSpaceAdjustArray[i + 1];
  925. if (i == stopIndex - 1 && hyphenFollows) {
  926. //the letter adjust here needs to be handled further down during
  927. //element generation because it depends on hyph/no-hyph condition
  928. letterSpaceAdjust = null;
  929. }
  930. if (letterSpaceAdjust != null) {
  931. newIPD = newIPD.plus(letterSpaceAdjust);
  932. }
  933. }
  934. }
  935. // add letter spaces
  936. boolean isWordEnd
  937. = (stopIndex == mapping.endIndex)
  938. && (mapping.letterSpaceCount < mapping.getWordLength());
  939. int letterSpaceCount = isWordEnd ? stopIndex - startIndex - 1 : stopIndex - startIndex;
  940. assert letterSpaceCount >= 0;
  941. newIPD = newIPD.plus(letterSpaceIPD.mult(letterSpaceCount));
  942. if (!(nothingChanged && stopIndex == mapping.endIndex && !hyphenFollows)) {
  943. // the new GlyphMapping object is not equal to the old one
  944. changeList.add(
  945. new PendingChange(
  946. new GlyphMapping(startIndex, stopIndex, 0,
  947. letterSpaceCount, newIPD, hyphenFollows,
  948. false, false, font, -1, null),
  949. ((LeafPosition) pos).getLeafPos() + changeOffset));
  950. nothingChanged = false;
  951. }
  952. startIndex = stopIndex;
  953. }
  954. hasChanged |= !nothingChanged;
  955. }
  956. /** {@inheritDoc} */
  957. public boolean applyChanges(final List oldList) {
  958. return applyChanges(oldList, 0);
  959. }
  960. /** {@inheritDoc} */
  961. public boolean applyChanges(final List oldList, int depth) {
  962. // make sure the LM appears unfinished in between this call
  963. // and the next call to getChangedKnuthElements()
  964. setFinished(false);
  965. if (oldList.isEmpty()) {
  966. return false;
  967. }
  968. // Find the first and last positions in oldList that point to a GlyphMapping
  969. // (i.e. getLeafPos() != -1)
  970. LeafPosition startPos = null;
  971. LeafPosition endPos = null;
  972. ListIterator oldListIter;
  973. for (oldListIter = oldList.listIterator(); oldListIter.hasNext();) {
  974. Position pos = ((KnuthElement) oldListIter.next()).getPosition();
  975. Position innerPosition = pos.getPosition(depth);
  976. assert (innerPosition == null || innerPosition instanceof LeafPosition);
  977. startPos = (LeafPosition) innerPosition;
  978. if (startPos != null && startPos.getLeafPos() != -1) {
  979. break;
  980. }
  981. }
  982. for (oldListIter = oldList.listIterator(oldList.size()); oldListIter.hasPrevious();) {
  983. Position pos = ((KnuthElement) oldListIter.previous()).getPosition();
  984. Position innerPosition = pos.getPosition(depth);
  985. assert (innerPosition instanceof LeafPosition);
  986. endPos = (LeafPosition) innerPosition;
  987. if (endPos != null && endPos.getLeafPos() != -1) {
  988. break;
  989. }
  990. }
  991. // set start/end index, taking into account any offset due to
  992. // changes applied to previous paragraphs
  993. returnedIndices[0] = (startPos != null ? startPos.getLeafPos() : -1) + changeOffset;
  994. returnedIndices[1] = (endPos != null ? endPos.getLeafPos() : -1) + changeOffset;
  995. int mappingsAdded = 0;
  996. int mappingsRemoved = 0;
  997. if (!changeList.isEmpty()) {
  998. int oldIndex = -1;
  999. int changeIndex;
  1000. PendingChange currChange;
  1001. for (Object aChangeList : changeList) {
  1002. currChange = (PendingChange) aChangeList;
  1003. if (currChange.index == oldIndex) {
  1004. mappingsAdded++;
  1005. changeIndex = currChange.index + mappingsAdded - mappingsRemoved;
  1006. } else {
  1007. mappingsRemoved++;
  1008. mappingsAdded++;
  1009. oldIndex = currChange.index;
  1010. changeIndex = currChange.index + mappingsAdded - mappingsRemoved;
  1011. removeGlyphMapping(changeIndex);
  1012. }
  1013. addGlyphMapping(changeIndex, currChange.mapping);
  1014. }
  1015. changeList.clear();
  1016. }
  1017. // increase the end index for getChangedKnuthElements()
  1018. returnedIndices[1] += (mappingsAdded - mappingsRemoved);
  1019. // increase offset to use for subsequent paragraphs
  1020. changeOffset += (mappingsAdded - mappingsRemoved);
  1021. return hasChanged;
  1022. }
  1023. /** {@inheritDoc} */
  1024. public List getChangedKnuthElements(final List oldList, final int alignment) {
  1025. if (isFinished()) {
  1026. return null;
  1027. }
  1028. final LinkedList returnList = new LinkedList();
  1029. for (; returnedIndices[0] <= returnedIndices[1]; returnedIndices[0]++) {
  1030. GlyphMapping mapping = getGlyphMapping(returnedIndices[0]);
  1031. if (mapping.wordSpaceCount == 0) {
  1032. // mapping refers either to a word or a word fragment
  1033. addElementsForAWordFragment(returnList, alignment, mapping, returnedIndices[0]);
  1034. } else {
  1035. // mapping refers to a space
  1036. addElementsForASpace(returnList, alignment, mapping, returnedIndices[0]);
  1037. }
  1038. }
  1039. setFinished(returnedIndices[0] == mappings.size() - 1);
  1040. //ElementListObserver.observe(returnList, "text-changed", null);
  1041. return returnList;
  1042. }
  1043. /** {@inheritDoc} */
  1044. public String getWordChars(Position pos) {
  1045. int leafValue = ((LeafPosition) pos).getLeafPos() + changeOffset;
  1046. if (leafValue != -1) {
  1047. GlyphMapping mapping = getGlyphMapping(leafValue);
  1048. StringBuffer buffer = new StringBuffer(mapping.getWordLength());
  1049. for (int i = mapping.startIndex; i < mapping.endIndex; i++) {
  1050. buffer.append(foText.charAt(i));
  1051. }
  1052. return buffer.toString();
  1053. } else {
  1054. return "";
  1055. }
  1056. }
  1057. private void addElementsForASpace(List baseList, int alignment, GlyphMapping mapping,
  1058. int leafValue) {
  1059. LeafPosition mainPosition = new LeafPosition(this, leafValue);
  1060. if (!mapping.breakOppAfter) {
  1061. // a non-breaking space
  1062. if (alignment == Constants.EN_JUSTIFY) {
  1063. // the space can stretch and shrink, and must be preserved
  1064. // when starting a line
  1065. baseList.add(makeAuxiliaryZeroWidthBox());
  1066. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1067. baseList.add(new KnuthGlue(mapping.areaIPD, mainPosition, false));
  1068. } else {
  1069. // the space does not need to stretch or shrink, and must be
  1070. // preserved when starting a line
  1071. baseList.add(new KnuthInlineBox(mapping.areaIPD.getOpt(), null, mainPosition,
  1072. true));
  1073. }
  1074. } else {
  1075. if (foText.charAt(mapping.startIndex) != CharUtilities.SPACE
  1076. || foText.getWhitespaceTreatment() == Constants.EN_PRESERVE) {
  1077. // a breaking space that needs to be preserved
  1078. baseList.addAll(getElementsForBreakingSpace(alignment, mapping, auxiliaryPosition, 0,
  1079. mainPosition, mapping.areaIPD.getOpt(), true));
  1080. } else {
  1081. // a (possible block) of breaking spaces
  1082. baseList.addAll(getElementsForBreakingSpace(alignment, mapping, mainPosition,
  1083. mapping.areaIPD.getOpt(), auxiliaryPosition, 0, false));
  1084. }
  1085. }
  1086. }
  1087. private List getElementsForBreakingSpace(int alignment, GlyphMapping mapping, Position pos2,
  1088. int p2WidthOffset, Position pos3,
  1089. int p3WidthOffset, boolean skipZeroCheck) {
  1090. List elements = new ArrayList();
  1091. switch (alignment) {
  1092. case EN_CENTER:
  1093. // centered text:
  1094. // if the second element is chosen as a line break these elements
  1095. // add a constant amount of stretch at the end of a line and at the
  1096. // beginning of the next one, otherwise they don't add any stretch
  1097. elements.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1098. auxiliaryPosition, false));
  1099. elements.add(makeZeroWidthPenalty(0));
  1100. elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP), -6
  1101. * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false));
  1102. elements.add(makeAuxiliaryZeroWidthBox());
  1103. elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1104. elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset,
  1105. 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos3, false));
  1106. break;
  1107. case EN_START: // fall through
  1108. case EN_END:
  1109. // left- or right-aligned text:
  1110. // if the second element is chosen as a line break these elements
  1111. // add a constant amount of stretch at the end of a line, otherwise
  1112. // they don't add any stretch
  1113. KnuthGlue g;
  1114. if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) {
  1115. g = new KnuthGlue(
  1116. lineEndBAP,
  1117. 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false);
  1118. elements.add(g);
  1119. elements.add(makeZeroWidthPenalty(0));
  1120. g = new KnuthGlue(
  1121. p2WidthOffset - (lineStartBAP + lineEndBAP),
  1122. -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false);
  1123. elements.add(g);
  1124. elements.add(makeAuxiliaryZeroWidthBox());
  1125. elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1126. g = new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false);
  1127. elements.add(g);
  1128. } else {
  1129. g = new KnuthGlue(
  1130. 0,
  1131. 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, auxiliaryPosition, false);
  1132. elements.add(g);
  1133. elements.add(makeZeroWidthPenalty(0));
  1134. g = new KnuthGlue(
  1135. mapping.areaIPD.getOpt(),
  1136. -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0, pos2, false);
  1137. elements.add(g);
  1138. }
  1139. break;
  1140. case EN_JUSTIFY:
  1141. // justified text:
  1142. // the stretch and shrink depends on the space width
  1143. elements.addAll(getElementsForJustifiedText(mapping, pos2, p2WidthOffset, pos3,
  1144. p3WidthOffset, skipZeroCheck, mapping.areaIPD.getShrink()));
  1145. break;
  1146. default:
  1147. // last line justified, the other lines unjustified:
  1148. // use only the space stretch
  1149. elements.addAll(getElementsForJustifiedText(mapping, pos2, p2WidthOffset, pos3,
  1150. p3WidthOffset, skipZeroCheck, 0));
  1151. }
  1152. return elements;
  1153. }
  1154. private List getElementsForJustifiedText(GlyphMapping mapping, Position pos2, int p2WidthOffset,
  1155. Position pos3, int p3WidthOffset, boolean skipZeroCheck,
  1156. int shrinkability) {
  1157. int stretchability = mapping.areaIPD.getStretch();
  1158. List elements = new ArrayList();
  1159. if (skipZeroCheck || lineStartBAP != 0 || lineEndBAP != 0) {
  1160. elements.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false));
  1161. elements.add(makeZeroWidthPenalty(0));
  1162. elements.add(new KnuthGlue(p2WidthOffset - (lineStartBAP + lineEndBAP),
  1163. stretchability, shrinkability, pos2, false));
  1164. elements.add(makeAuxiliaryZeroWidthBox());
  1165. elements.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1166. elements.add(new KnuthGlue(lineStartBAP + p3WidthOffset, 0, 0, pos3, false));
  1167. } else {
  1168. elements.add(new KnuthGlue(mapping.areaIPD.getOpt(), stretchability, shrinkability,
  1169. pos2, false));
  1170. }
  1171. return elements;
  1172. }
  1173. private void addElementsForAWordFragment(List baseList, int alignment, GlyphMapping mapping,
  1174. int leafValue) {
  1175. LeafPosition mainPosition = new LeafPosition(this, leafValue);
  1176. // if the last character of the word fragment is '-' or '/',
  1177. // the fragment could end a line; in this case, it loses one
  1178. // of its letter spaces;
  1179. boolean suppressibleLetterSpace = mapping.breakOppAfter && !mapping.isHyphenated;
  1180. if (letterSpaceIPD.isStiff()) {
  1181. // constant letter spacing
  1182. baseList.add(new KnuthInlineBox(suppressibleLetterSpace
  1183. ? mapping.areaIPD.getOpt() - letterSpaceIPD.getOpt()
  1184. : mapping.areaIPD.getOpt(),
  1185. alignmentContext, notifyPos(mainPosition), false));
  1186. } else {
  1187. // adjustable letter spacing
  1188. int unsuppressibleLetterSpaces = suppressibleLetterSpace
  1189. ? mapping.letterSpaceCount - 1
  1190. : mapping.letterSpaceCount;
  1191. baseList.add(new KnuthInlineBox(mapping.areaIPD.getOpt()
  1192. - mapping.letterSpaceCount * letterSpaceIPD.getOpt(),
  1193. alignmentContext, notifyPos(mainPosition), false));
  1194. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1195. baseList.add(new KnuthGlue(letterSpaceIPD.mult(unsuppressibleLetterSpaces),
  1196. auxiliaryPosition, true));
  1197. baseList.add(makeAuxiliaryZeroWidthBox());
  1198. }
  1199. // extra-elements if the word fragment is the end of a syllable,
  1200. // or it ends with a character that can be used as a line break
  1201. if (mapping.isHyphenated) {
  1202. MinOptMax widthIfNoBreakOccurs = null;
  1203. if (mapping.endIndex < foText.length()) {
  1204. //Add in kerning in no-break condition
  1205. widthIfNoBreakOccurs = letterSpaceAdjustArray[mapping.endIndex];
  1206. }
  1207. //if (mapping.breakIndex)
  1208. // the word fragment ends at the end of a syllable:
  1209. // if a break occurs the content width increases,
  1210. // otherwise nothing happens
  1211. addElementsForAHyphen(baseList, alignment, hyphIPD, widthIfNoBreakOccurs,
  1212. mapping.breakOppAfter);
  1213. } else if (suppressibleLetterSpace) {
  1214. // the word fragment ends with a character that acts as a hyphen
  1215. // if a break occurs the width does not increase,
  1216. // otherwise there is one more letter space
  1217. addElementsForAHyphen(baseList, alignment, 0, letterSpaceIPD, true);
  1218. }
  1219. }
  1220. private void addElementsForAHyphen(List baseList, int alignment, int widthIfBreakOccurs,
  1221. MinOptMax widthIfNoBreakOccurs, boolean unflagged) {
  1222. if (widthIfNoBreakOccurs == null) {
  1223. widthIfNoBreakOccurs = MinOptMax.ZERO;
  1224. }
  1225. switch (alignment) {
  1226. case EN_CENTER:
  1227. // centered text:
  1228. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1229. baseList.add(new KnuthGlue(lineEndBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1230. auxiliaryPosition, true));
  1231. baseList.add(new KnuthPenalty(hyphIPD, unflagged
  1232. ? TextLayoutManager.SOFT_HYPHEN_PENALTY
  1233. : KnuthPenalty.FLAGGED_PENALTY, !unflagged, auxiliaryPosition, false));
  1234. baseList.add(new KnuthGlue(-(lineEndBAP + lineStartBAP),
  1235. -6 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1236. auxiliaryPosition, false));
  1237. baseList.add(makeAuxiliaryZeroWidthBox());
  1238. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1239. baseList.add(new KnuthGlue(lineStartBAP, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH,
  1240. 0, auxiliaryPosition, true));
  1241. break;
  1242. case EN_START: // fall through
  1243. case EN_END:
  1244. // left- or right-aligned text:
  1245. if (lineStartBAP != 0 || lineEndBAP != 0) {
  1246. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1247. baseList.add(new KnuthGlue(lineEndBAP,
  1248. 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1249. auxiliaryPosition, false));
  1250. baseList.add(new KnuthPenalty(widthIfBreakOccurs,
  1251. unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
  1252. : KnuthPenalty.FLAGGED_PENALTY, !unflagged,
  1253. auxiliaryPosition, false));
  1254. baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt()
  1255. - (lineStartBAP + lineEndBAP),
  1256. -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1257. auxiliaryPosition, false));
  1258. baseList.add(makeAuxiliaryZeroWidthBox());
  1259. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1260. baseList.add(new KnuthGlue(lineStartBAP, 0, 0, auxiliaryPosition, false));
  1261. } else {
  1262. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1263. baseList.add(new KnuthGlue(0, 3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1264. auxiliaryPosition, false));
  1265. baseList.add(new KnuthPenalty(widthIfBreakOccurs,
  1266. unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
  1267. : KnuthPenalty.FLAGGED_PENALTY, !unflagged,
  1268. auxiliaryPosition, false));
  1269. baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt(),
  1270. -3 * LineLayoutManager.DEFAULT_SPACE_WIDTH, 0,
  1271. auxiliaryPosition, false));
  1272. }
  1273. break;
  1274. default:
  1275. // justified text, or last line justified:
  1276. // just a flagged penalty
  1277. if (lineStartBAP != 0 || lineEndBAP != 0) {
  1278. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1279. baseList.add(new KnuthGlue(lineEndBAP, 0, 0, auxiliaryPosition, false));
  1280. baseList.add(new KnuthPenalty(widthIfBreakOccurs,
  1281. unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
  1282. : KnuthPenalty.FLAGGED_PENALTY, !unflagged,
  1283. auxiliaryPosition, false));
  1284. // extra elements representing a letter space that is suppressed
  1285. // if a break occurs
  1286. if (widthIfNoBreakOccurs.isNonZero()) {
  1287. baseList.add(new KnuthGlue(widthIfNoBreakOccurs.getOpt()
  1288. - (lineStartBAP + lineEndBAP),
  1289. widthIfNoBreakOccurs.getStretch(),
  1290. widthIfNoBreakOccurs.getShrink(),
  1291. auxiliaryPosition, false));
  1292. } else {
  1293. baseList.add(new KnuthGlue(-(lineStartBAP + lineEndBAP), 0, 0,
  1294. auxiliaryPosition, false));
  1295. }
  1296. baseList.add(makeAuxiliaryZeroWidthBox());
  1297. baseList.add(makeZeroWidthPenalty(KnuthElement.INFINITE));
  1298. baseList.add(new KnuthGlue(lineStartBAP, 0, 0,
  1299. auxiliaryPosition, false));
  1300. } else {
  1301. baseList.add(new KnuthPenalty(widthIfBreakOccurs,
  1302. unflagged ? TextLayoutManager.SOFT_HYPHEN_PENALTY
  1303. : KnuthPenalty.FLAGGED_PENALTY, !unflagged,
  1304. auxiliaryPosition, false));
  1305. // extra elements representing a letter space that is suppressed
  1306. // if a break occurs
  1307. if (widthIfNoBreakOccurs.isNonZero()) {
  1308. baseList.add(new KnuthGlue(widthIfNoBreakOccurs, auxiliaryPosition, false));
  1309. }
  1310. }
  1311. }
  1312. }
  1313. /** {@inheritDoc} */
  1314. public String toString() {
  1315. return super.toString() + "{"
  1316. + "chars = \'"
  1317. + CharUtilities.toNCRefs(foText.getCharSequence().toString())
  1318. + "\'"
  1319. + ", len = " + foText.length()
  1320. + "}";
  1321. }
  1322. }