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 }