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 }