View Javadoc

1   /*
2    * Copyright 2002 - 2009 JEuclid, http://jeuclid.sf.net
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  /* $Id: StringUtil.java,v 74b8e95997bf 2010/08/11 17:45:46 max $ */
18  
19  package net.sourceforge.jeuclid.elements.support.text;
20  
21  import java.awt.Font;
22  import java.awt.Graphics2D;
23  import java.awt.font.FontRenderContext;
24  import java.awt.font.TextAttribute;
25  import java.awt.font.TextLayout;
26  import java.awt.geom.Rectangle2D;
27  import java.text.AttributedCharacterIterator;
28  import java.text.AttributedString;
29  import java.text.CharacterIterator;
30  import java.util.ArrayList;
31  import java.util.Iterator;
32  import java.util.List;
33  
34  import javax.annotation.Nullable;
35  
36  import net.sourceforge.jeuclid.LayoutContext;
37  import net.sourceforge.jeuclid.context.Parameter;
38  import net.sourceforge.jeuclid.elements.AbstractJEuclidElement;
39  import net.sourceforge.jeuclid.elements.JEuclidElement;
40  import net.sourceforge.jeuclid.elements.support.GraphicsSupport;
41  import net.sourceforge.jeuclid.elements.support.attributes.MathVariant;
42  
43  import org.w3c.dom.Element;
44  import org.w3c.dom.Node;
45  import org.w3c.dom.NodeList;
46  
47  /**
48   * Utilities for String handling.
49   *
50   * @version $Revision: 74b8e95997bf $
51   */
52  // CHECKSTYLE:OFF
53  // Data Abstraction Coupling is too high. Hover, String handling is not
54  // simple.
55  public final class StringUtil {
56      // CHECKSTYLE:ON
57  
58      /**
59       * Set to true if we're running under Mac OS X.
60       */
61      public static final boolean OSX = System.getProperty("mrj.version") != null; //$NON-NLS-1$
62  
63      static final CharacterMapping CMAP = CharacterMapping.getInstance();
64  
65      private StringUtil() {
66          // do nothing
67      }
68  
69      /**
70       * Converts a given String to an attributed string with the proper variants
71       * set.
72       *
73       * @param inputString
74       *            the string to convert.
75       * @param baseVariant
76       *            variant to base on for regular characters
77       * @param fontSize
78       *            size of Font to use.
79       * @param context
80       *            Layout Context to use.
81       * @return an attributed string that has Textattribute.FONT set for all
82       *         characters.
83       */
84      public static AttributedString convertStringtoAttributedString(
85              final String inputString, final MathVariant baseVariant,
86              final float fontSize, final LayoutContext context) {
87          if (inputString == null) {
88              return new AttributedString("");
89          }
90          final StringBuilder builder = new StringBuilder();
91          final List<Font> fonts = new ArrayList<Font>();
92          final String plainString = CharConverter.convertLate(inputString);
93  
94          for (int i = 0; i < plainString.length(); i++) {
95              if (!Character.isLowSurrogate(plainString.charAt(i))) {
96  
97                  final CodePointAndVariant cpav1 = new CodePointAndVariant(
98                          plainString.codePointAt(i), baseVariant);
99                  final Object[] codeAndFont = StringUtil.mapCpavToCpaf(cpav1,
100                         fontSize, context);
101                 final int codePoint = (Integer) codeAndFont[0];
102                 final Font font = (Font) codeAndFont[1];
103 
104                 builder.appendCodePoint(codePoint);
105                 fonts.add(font);
106                 if (Character.isSupplementaryCodePoint(codePoint)) {
107                     fonts.add(font);
108                 }
109             }
110         }
111 
112         final AttributedString aString = new AttributedString(builder
113                 .toString());
114 
115         final int len = builder.length();
116 
117         for (int i = 0; i < len; i++) {
118             final char currentChar = builder.charAt(i);
119             if (!Character.isLowSurrogate(currentChar)) {
120                 final Font font = fonts.get(i);
121                 final int count;
122                 if (Character.isHighSurrogate(currentChar)) {
123                     count = 2;
124                 } else {
125                     count = 1;
126                 }
127                 aString.addAttribute(TextAttribute.FONT, font, i, i + count);
128             }
129         }
130         return aString;
131     }
132 
133     /**
134      * Provide the text content of the current element as
135      * AttributedCharacterIterator.
136      *
137      * @param contextNow
138      *            LayoutContext of the parent element.
139      * @param contextElement
140      *            Parent Element.
141      * @param node
142      *            Current node.
143      * @param corrector
144      *            Font-size corrector.
145      * @return An {@link AttributedCharacterIterator} over the text contents.
146      */
147     public static AttributedCharacterIterator textContentAsAttributedCharacterIterator(
148             final LayoutContext contextNow,
149             final JEuclidElement contextElement, final Node node,
150             final float corrector) {
151         AttributedCharacterIterator retVal;
152         if (node instanceof Element) {
153 
154             final MultiAttributedCharacterIterator maci = new MultiAttributedCharacterIterator();
155             final NodeList children = node.getChildNodes();
156             AttributedCharacterIterator aci = null;
157             final int childCount = children.getLength();
158             for (int i = 0; i < childCount; i++) {
159                 final LayoutContext subContext;
160                 final Node child = children.item(i);
161                 final JEuclidElement subContextElement;
162                 if (child instanceof AbstractJEuclidElement) {
163                     subContext = ((AbstractJEuclidElement) child)
164                             .applyLocalAttributesToContext(contextNow);
165                     subContextElement = (JEuclidElement) child;
166                 } else {
167                     subContext = contextNow;
168                     subContextElement = contextElement;
169                 }
170                 aci = StringUtil.textContentAsAttributedCharacterIterator(
171                         subContext, subContextElement, child, corrector);
172                 maci.appendAttributedCharacterIterator(aci);
173             }
174 
175             if (childCount != 1) {
176                 aci = maci;
177             }
178 
179             if (node instanceof TextContentModifier) {
180                 final TextContentModifier t = (TextContentModifier) node;
181                 retVal = t.modifyTextContent(aci, contextNow);
182             } else {
183                 retVal = aci;
184             }
185         } else {
186             final String theText = TextContent.getText(node);
187             final float fontSizeInPoint = GraphicsSupport
188                     .getFontsizeInPoint(contextNow)
189                     * corrector;
190 
191             retVal = StringUtil.convertStringtoAttributedString(theText,
192                     contextElement.getMathvariantAsVariant(), fontSizeInPoint,
193                     contextNow).getIterator();
194         }
195         return retVal;
196     }
197 
198     private static Object[] mapCpavToCpaf(final CodePointAndVariant cpav1,
199             final float fontSize, final LayoutContext context) {
200         final List<CodePointAndVariant> alternatives = StringUtil.CMAP
201                 .getAllAlternatives(cpav1);
202 
203         Font font = null;
204         int codePoint = 0;
205         final Iterator<CodePointAndVariant> it = alternatives.iterator();
206         boolean cont = true;
207         while (cont) {
208             final CodePointAndVariant cpav = it.next();
209             if (it.hasNext()) {
210                 codePoint = cpav.getCodePoint();
211                 font = cpav.getVariant().createFont(fontSize, codePoint,
212                         context, false);
213                 if (font != null) {
214                     cont = false;
215                 }
216             } else {
217                 codePoint = cpav.getCodePoint();
218                 font = cpav.getVariant().createFont(fontSize, codePoint,
219                         context, true);
220                 cont = false;
221             }
222         }
223         return new Object[] { codePoint, font };
224     }
225 
226     /**
227      * Safely creates a Text Layout from an attributed string. Unlike the
228      * TextLayout constructor, the String here may actually be empty.
229      *
230      * @param g
231      *            Graphics context.
232      * @param aString
233      *            an Attributed String
234      * @param context
235      *            Layout Context to use.
236      * @return a TextLayout
237      */
238     public static TextLayout createTextLayoutFromAttributedString(
239             final Graphics2D g, final AttributedString aString,
240             final LayoutContext context) {
241         final AttributedCharacterIterator charIter = aString.getIterator();
242         final boolean empty = charIter.first() == CharacterIterator.DONE;
243         final FontRenderContext suggestedFontRenderContext = g
244                 .getFontRenderContext();
245         boolean antialiasing = (Boolean) context
246                 .getParameter(Parameter.ANTIALIAS);
247         if (!empty) {
248             final Font font = (Font) aString.getIterator().getAttribute(
249                     TextAttribute.FONT);
250             if (font != null) {
251                 final float fontsize = font.getSize2D();
252                 final float minantialias = (Float) context
253                         .getParameter(Parameter.ANTIALIAS_MINSIZE);
254                 antialiasing &= fontsize >= minantialias;
255             }
256         }
257 
258         final FontRenderContext realFontRenderContext = new FontRenderContext(
259                 suggestedFontRenderContext.getTransform(), antialiasing, false);
260 
261         final TextLayout theLayout;
262         if (empty) {
263             theLayout = new TextLayout(" ", new Font("", 0, 0),
264                     realFontRenderContext);
265         } else {
266             synchronized (TextLayout.class) {
267                 // Catches a rare NullPointerException in
268                 // sun.font.FileFontStrike.getCachedGlyphPtr(FileFontStrike.java:448)
269                 theLayout = new TextLayout(aString.getIterator(),
270                         realFontRenderContext);
271             }
272         }
273         return theLayout;
274     }
275 
276     /**
277      * Retrieves the real width from a given text layout.
278      *
279      * @param layout
280      *            the textlayout
281      * @return width
282      */
283     public static float getWidthForTextLayout(final TextLayout layout) {
284         final Rectangle2D r2d = layout.getBounds();
285         float realWidth = (float) r2d.getWidth();
286         final float xo = (float) r2d.getX();
287         if (xo > 0) {
288             realWidth += xo;
289         }
290         // Unfortunately this is necessary, although it does not look like it
291         // makes a lot of sense.
292         final float invisibleAdvance = layout.getAdvance()
293                 - layout.getVisibleAdvance();
294         return realWidth + invisibleAdvance;
295     }
296 
297     /**
298      * Contains layout information retrieved from a TextLayout.
299      */
300     public static class TextLayoutInfo {
301         private final float ascent;
302 
303         private final float descent;
304 
305         private final float offset;
306 
307         private final float width;
308 
309         /**
310          * Default Constructor.
311          *
312          * @param newAscent
313          *            text ascent.
314          * @param newDescent
315          *            text descent.
316          * @param newOffset
317          *            text start offset.
318          * @param newWidth
319          *            text width.
320          */
321         protected TextLayoutInfo(final float newAscent, final float newDescent,
322                 final float newOffset, final float newWidth) {
323             this.ascent = newAscent;
324             this.descent = newDescent;
325             this.offset = newOffset;
326             this.width = newWidth;
327         }
328 
329         /**
330          * Getter method for ascent.
331          *
332          * @return the ascent
333          */
334         public float getAscent() {
335             return this.ascent;
336         }
337 
338         /**
339          * Getter method for descent.
340          *
341          * @return the descent
342          */
343         public float getDescent() {
344             return this.descent;
345         }
346 
347         /**
348          * Getter method for offset.
349          *
350          * @return the offset
351          */
352         public float getOffset() {
353             return this.offset;
354         }
355 
356         /**
357          * Getter method for width.
358          *
359          * @return the width
360          */
361         public float getWidth() {
362             return this.width;
363         }
364 
365     };
366 
367     /**
368      * Retrieve the actual layout information from a textLayout. This is
369      * different than the values given when calling the functions directly.
370      *
371      * @param textLayout
372      *            TextLayout to look at.
373      * @param trim
374      *            Trim to actual content
375      * @return a TextLayoutInfo.
376      */
377     public static TextLayoutInfo getTextLayoutInfo(final TextLayout textLayout,
378             final boolean trim) {
379         final Rectangle2D textBounds = textLayout.getBounds();
380         final float ascent = (float) (-textBounds.getY());
381         final float descent = (float) (textBounds.getY() + textBounds
382                 .getHeight());
383         final float xo = (float) textBounds.getX();
384         final float xOffset;
385         if (xo < 0) {
386             xOffset = -xo;
387         } else {
388             if (trim) {
389                 xOffset = -xo;
390             } else {
391                 xOffset = 0.0f;
392             }
393         }
394         final float width = StringUtil.getWidthForTextLayout(textLayout);
395         return new TextLayoutInfo(ascent, descent, xOffset, width);
396     }
397 
398     /**
399      * Counts the displayable characters only. Counts high-surrogates as one
400      * character. Also, ignores combining mark characters.
401      *
402      * @param s
403      *            string to count length of.
404      * @return display length of the string.
405      */
406     public static int countDisplayableCharacters(@Nullable final String s) {
407         if (s == null) {
408             return 0;
409         }
410         int length = 0;
411         final int strLen = s.length();
412         for (int i = 0; i < strLen; i++) {
413             final char charHere = s.charAt(i);
414             if (!Character.isHighSurrogate(charHere)) {
415                 final int codepoint = s.codePointAt(i);
416                 if (!StringUtil.CMAP.isMark(codepoint)) {
417                     length++;
418                 }
419             }
420         }
421         return length;
422     }
423 
424 }