001    /*
002     * Copyright 2002 - 2009 JEuclid, http://jeuclid.sf.net
003     *
004     * Licensed under the Apache License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    
017    /* $Id: StringUtil.java,v 74b8e95997bf 2010/08/11 17:45:46 max $ */
018    
019    package net.sourceforge.jeuclid.elements.support.text;
020    
021    import java.awt.Font;
022    import java.awt.Graphics2D;
023    import java.awt.font.FontRenderContext;
024    import java.awt.font.TextAttribute;
025    import java.awt.font.TextLayout;
026    import java.awt.geom.Rectangle2D;
027    import java.text.AttributedCharacterIterator;
028    import java.text.AttributedString;
029    import java.text.CharacterIterator;
030    import java.util.ArrayList;
031    import java.util.Iterator;
032    import java.util.List;
033    
034    import javax.annotation.Nullable;
035    
036    import net.sourceforge.jeuclid.LayoutContext;
037    import net.sourceforge.jeuclid.context.Parameter;
038    import net.sourceforge.jeuclid.elements.AbstractJEuclidElement;
039    import net.sourceforge.jeuclid.elements.JEuclidElement;
040    import net.sourceforge.jeuclid.elements.support.GraphicsSupport;
041    import net.sourceforge.jeuclid.elements.support.attributes.MathVariant;
042    
043    import org.w3c.dom.Element;
044    import org.w3c.dom.Node;
045    import org.w3c.dom.NodeList;
046    
047    /**
048     * Utilities for String handling.
049     *
050     * @version $Revision: 74b8e95997bf $
051     */
052    // CHECKSTYLE:OFF
053    // Data Abstraction Coupling is too high. Hover, String handling is not
054    // simple.
055    public final class StringUtil {
056        // CHECKSTYLE:ON
057    
058        /**
059         * Set to true if we're running under Mac OS X.
060         */
061        public static final boolean OSX = System.getProperty("mrj.version") != null; //$NON-NLS-1$
062    
063        static final CharacterMapping CMAP = CharacterMapping.getInstance();
064    
065        private StringUtil() {
066            // do nothing
067        }
068    
069        /**
070         * Converts a given String to an attributed string with the proper variants
071         * set.
072         *
073         * @param inputString
074         *            the string to convert.
075         * @param baseVariant
076         *            variant to base on for regular characters
077         * @param fontSize
078         *            size of Font to use.
079         * @param context
080         *            Layout Context to use.
081         * @return an attributed string that has Textattribute.FONT set for all
082         *         characters.
083         */
084        public static AttributedString convertStringtoAttributedString(
085                final String inputString, final MathVariant baseVariant,
086                final float fontSize, final LayoutContext context) {
087            if (inputString == null) {
088                return new AttributedString("");
089            }
090            final StringBuilder builder = new StringBuilder();
091            final List<Font> fonts = new ArrayList<Font>();
092            final String plainString = CharConverter.convertLate(inputString);
093    
094            for (int i = 0; i < plainString.length(); i++) {
095                if (!Character.isLowSurrogate(plainString.charAt(i))) {
096    
097                    final CodePointAndVariant cpav1 = new CodePointAndVariant(
098                            plainString.codePointAt(i), baseVariant);
099                    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    }