001/* 002 * SVG Salamander 003 * Copyright (c) 2004, Mark McKay 004 * All rights reserved. 005 * 006 * Redistribution and use in source and binary forms, with or 007 * without modification, are permitted provided that the following 008 * conditions are met: 009 * 010 * - Redistributions of source code must retain the above 011 * copyright notice, this list of conditions and the following 012 * disclaimer. 013 * - Redistributions in binary form must reproduce the above 014 * copyright notice, this list of conditions and the following 015 * disclaimer in the documentation and/or other materials 016 * provided with the distribution. 017 * 018 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 019 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 020 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 021 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 022 * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 023 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 025 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 026 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 027 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 028 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 029 * OF THE POSSIBILITY OF SUCH DAMAGE. 030 * 031 * Mark McKay can be contacted at mark@kitfox.com. Salamander and other 032 * projects can be found at http://www.kitfox.com 033 * 034 * Created on January 26, 2004, 1:56 AM 035 */ 036package com.kitfox.svg; 037 038import com.kitfox.svg.xml.StyleAttribute; 039import java.awt.Graphics2D; 040import java.awt.Shape; 041import java.awt.font.FontRenderContext; 042import java.awt.geom.AffineTransform; 043import java.awt.geom.GeneralPath; 044import java.awt.geom.Point2D; 045import java.awt.geom.Rectangle2D; 046import java.util.Iterator; 047import java.util.LinkedList; 048import java.util.regex.Matcher; 049import java.util.regex.Pattern; 050 051//import org.apache.batik.ext.awt.geom.ExtendedGeneralPath; 052/** 053 * @author Mark McKay 054 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a> 055 */ 056public class Text extends ShapeElement 057{ 058 public static final String TAG_NAME = "text"; 059 060 float x = 0; 061 float y = 0; 062 AffineTransform transform = null; 063 String fontFamily; 064 float fontSize; 065 //List of strings and tspans containing the content of this node 066 LinkedList content = new LinkedList(); 067 Shape textShape; 068 public static final int TXAN_START = 0; 069 public static final int TXAN_MIDDLE = 1; 070 public static final int TXAN_END = 2; 071 int textAnchor = TXAN_START; 072 public static final int TXST_NORMAL = 0; 073 public static final int TXST_ITALIC = 1; 074 public static final int TXST_OBLIQUE = 2; 075 int fontStyle; 076 public static final int TXWE_NORMAL = 0; 077 public static final int TXWE_BOLD = 1; 078 public static final int TXWE_BOLDER = 2; 079 public static final int TXWE_LIGHTER = 3; 080 public static final int TXWE_100 = 4; 081 public static final int TXWE_200 = 5; 082 public static final int TXWE_300 = 6; 083 public static final int TXWE_400 = 7; 084 public static final int TXWE_500 = 8; 085 public static final int TXWE_600 = 9; 086 public static final int TXWE_700 = 10; 087 public static final int TXWE_800 = 11; 088 public static final int TXWE_900 = 12; 089 int fontWeight; 090 091 /** 092 * Creates a new instance of Stop 093 */ 094 public Text() 095 { 096 } 097 098 public String getTagName() 099 { 100 return TAG_NAME; 101 } 102 103 public void appendText(String text) 104 { 105 content.addLast(text); 106 } 107 108 public void appendTspan(Tspan tspan) throws SVGElementException 109 { 110 super.loaderAddChild(null, tspan); 111 content.addLast(tspan); 112 } 113 114 /** 115 * Discard cached information 116 */ 117 public void rebuild() throws SVGException 118 { 119 build(); 120 } 121 122 public java.util.List getContent() 123 { 124 return content; 125 } 126 127 /** 128 * Called after the start element but before the end element to indicate 129 * each child tag that has been processed 130 */ 131 public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException 132 { 133 super.loaderAddChild(helper, child); 134 135 content.addLast(child); 136 } 137 138 /** 139 * Called during load process to add text scanned within a tag 140 */ 141 public void loaderAddText(SVGLoaderHelper helper, String text) 142 { 143 Matcher matchWs = Pattern.compile("\\s*").matcher(text); 144 if (!matchWs.matches()) 145 { 146 content.addLast(text); 147 } 148 } 149 150 public void build() throws SVGException 151 { 152 super.build(); 153 154 StyleAttribute sty = new StyleAttribute(); 155 156 if (getPres(sty.setName("x"))) 157 { 158 x = sty.getFloatValueWithUnits(); 159 } 160 161 if (getPres(sty.setName("y"))) 162 { 163 y = sty.getFloatValueWithUnits(); 164 } 165 166 if (getStyle(sty.setName("font-family"))) 167 { 168 fontFamily = sty.getStringValue(); 169 } else 170 { 171 fontFamily = "Sans Serif"; 172 } 173 174 if (getStyle(sty.setName("font-size"))) 175 { 176 fontSize = sty.getFloatValueWithUnits(); 177 } else 178 { 179 fontSize = 12f; 180 } 181 182 if (getStyle(sty.setName("font-style"))) 183 { 184 String s = sty.getStringValue(); 185 if ("normal".equals(s)) 186 { 187 fontStyle = TXST_NORMAL; 188 } else if ("italic".equals(s)) 189 { 190 fontStyle = TXST_ITALIC; 191 } else if ("oblique".equals(s)) 192 { 193 fontStyle = TXST_OBLIQUE; 194 } 195 } else 196 { 197 fontStyle = TXST_NORMAL; 198 } 199 200 if (getStyle(sty.setName("font-weight"))) 201 { 202 String s = sty.getStringValue(); 203 if ("normal".equals(s)) 204 { 205 fontWeight = TXWE_NORMAL; 206 } else if ("bold".equals(s)) 207 { 208 fontWeight = TXWE_BOLD; 209 } 210 } else 211 { 212 fontWeight = TXWE_NORMAL; 213 } 214 215 if (getStyle(sty.setName("text-anchor"))) 216 { 217 String s = sty.getStringValue(); 218 if (s.equals("middle")) 219 { 220 textAnchor = TXAN_MIDDLE; 221 } else if (s.equals("end")) 222 { 223 textAnchor = TXAN_END; 224 } else 225 { 226 textAnchor = TXAN_START; 227 } 228 } else 229 { 230 textAnchor = TXAN_START; 231 } 232 233 //text anchor 234 //text-decoration 235 //text-rendering 236 237 buildFont(); 238 } 239 240 protected void buildFont() throws SVGException 241 { 242 int style; 243 switch (fontStyle) 244 { 245 case TXST_ITALIC: 246 style = java.awt.Font.ITALIC; 247 break; 248 default: 249 style = java.awt.Font.PLAIN; 250 break; 251 } 252 253 int weight; 254 switch (fontWeight) 255 { 256 case TXWE_BOLD: 257 case TXWE_BOLDER: 258 weight = java.awt.Font.BOLD; 259 break; 260 default: 261 weight = java.awt.Font.PLAIN; 262 break; 263 } 264 265 //Get font 266 Font font = diagram.getUniverse().getFont(fontFamily); 267 if (font == null) 268 { 269// System.err.println("Could not load font"); 270 271 java.awt.Font sysFont = new java.awt.Font(fontFamily, style | weight, (int) fontSize); 272 buildSysFont(sysFont); 273 return; 274 } 275 276// font = new java.awt.Font(font.getFamily(), style | weight, font.getSize()); 277 278// Area textArea = new Area(); 279 GeneralPath textPath = new GeneralPath(); 280 textShape = textPath; 281 282 float cursorX = x, cursorY = y; 283 284 FontFace fontFace = font.getFontFace(); 285 //int unitsPerEm = fontFace.getUnitsPerEm(); 286 int ascent = fontFace.getAscent(); 287 float fontScale = fontSize / (float) ascent; 288 289// AffineTransform oldXform = g.getTransform(); 290 AffineTransform xform = new AffineTransform(); 291 292 for (Iterator it = content.iterator(); it.hasNext();) 293 { 294 Object obj = it.next(); 295 296 if (obj instanceof String) 297 { 298 String text = (String) obj; 299 if (text != null) 300 { 301 text = text.trim(); 302 } 303 304 strokeWidthScalar = 1f / fontScale; 305 306 for (int i = 0; i < text.length(); i++) 307 { 308 xform.setToIdentity(); 309 xform.setToTranslation(cursorX, cursorY); 310 xform.scale(fontScale, fontScale); 311// g.transform(xform); 312 313 String unicode = text.substring(i, i + 1); 314 MissingGlyph glyph = font.getGlyph(unicode); 315 316 Shape path = glyph.getPath(); 317 if (path != null) 318 { 319 path = xform.createTransformedShape(path); 320 textPath.append(path, false); 321 } 322// else glyph.render(g); 323 324 cursorX += fontScale * glyph.getHorizAdvX(); 325 326// g.setTransform(oldXform); 327 } 328 329 strokeWidthScalar = 1f; 330 } else if (obj instanceof Tspan) 331 { 332 Tspan tspan = (Tspan) obj; 333 334 xform.setToIdentity(); 335 xform.setToTranslation(cursorX, cursorY); 336 xform.scale(fontScale, fontScale); 337// tspan.setCursorX(cursorX); 338// tspan.setCursorY(cursorY); 339 340 Shape tspanShape = tspan.getShape(); 341 tspanShape = xform.createTransformedShape(tspanShape); 342 textPath.append(tspanShape, false); 343// tspan.render(g); 344// cursorX = tspan.getCursorX(); 345// cursorY = tspan.getCursorY(); 346 } 347 348 } 349 350 switch (textAnchor) 351 { 352 case TXAN_MIDDLE: 353 { 354 AffineTransform at = new AffineTransform(); 355 at.translate(-textPath.getBounds().getWidth() / 2, 0); 356 textPath.transform(at); 357 break; 358 } 359 case TXAN_END: 360 { 361 AffineTransform at = new AffineTransform(); 362 at.translate(-textPath.getBounds().getWidth(), 0); 363 textPath.transform(at); 364 break; 365 } 366 } 367 } 368 369 private void buildSysFont(java.awt.Font font) throws SVGException 370 { 371 GeneralPath textPath = new GeneralPath(); 372 textShape = textPath; 373 374 float cursorX = x, cursorY = y; 375 376// FontMetrics fm = g.getFontMetrics(font); 377 FontRenderContext frc = new FontRenderContext(null, true, true); 378 379// FontFace fontFace = font.getFontFace(); 380 //int unitsPerEm = fontFace.getUnitsPerEm(); 381// int ascent = fm.getAscent(); 382// float fontScale = fontSize / (float)ascent; 383 384// AffineTransform oldXform = g.getTransform(); 385 AffineTransform xform = new AffineTransform(); 386 387 for (Iterator it = content.iterator(); it.hasNext();) 388 { 389 Object obj = it.next(); 390 391 if (obj instanceof String) 392 { 393 String text = (String)obj; 394 text = text.trim(); 395 396 Shape textShape = font.createGlyphVector(frc, text).getOutline(cursorX, cursorY); 397 textPath.append(textShape, false); 398// renderShape(g, textShape); 399// g.drawString(text, cursorX, cursorY); 400 401 Rectangle2D rect = font.getStringBounds(text, frc); 402 cursorX += (float) rect.getWidth(); 403 } else if (obj instanceof Tspan) 404 { 405 /* 406 Tspan tspan = (Tspan)obj; 407 408 xform.setToIdentity(); 409 xform.setToTranslation(cursorX, cursorY); 410 411 Shape tspanShape = tspan.getShape(); 412 tspanShape = xform.createTransformedShape(tspanShape); 413 textArea.add(new Area(tspanShape)); 414 415 cursorX += tspanShape.getBounds2D().getWidth(); 416 */ 417 418 419 Tspan tspan = (Tspan)obj; 420 Point2D cursor = new Point2D.Float(cursorX, cursorY); 421// tspan.setCursorX(cursorX); 422// tspan.setCursorY(cursorY); 423 tspan.appendToShape(textPath, cursor); 424// cursorX = tspan.getCursorX(); 425// cursorY = tspan.getCursorY(); 426 cursorX = (float)cursor.getX(); 427 cursorY = (float)cursor.getY(); 428 429 } 430 } 431 432 switch (textAnchor) 433 { 434 case TXAN_MIDDLE: 435 { 436 AffineTransform at = new AffineTransform(); 437 at.translate(-textPath.getBounds().getWidth() / 2, 0); 438 textPath.transform(at); 439 break; 440 } 441 case TXAN_END: 442 { 443 AffineTransform at = new AffineTransform(); 444 at.translate(-Math.ceil(textPath.getBounds().getWidth()), 0); 445 textPath.transform(at); 446 break; 447 } 448 } 449 } 450 451 public void render(Graphics2D g) throws SVGException 452 { 453 beginLayer(g); 454 renderShape(g, textShape); 455 finishLayer(g); 456 } 457 458 public Shape getShape() 459 { 460 return shapeToParent(textShape); 461 } 462 463 public Rectangle2D getBoundingBox() throws SVGException 464 { 465 return boundsToParent(includeStrokeInBounds(textShape.getBounds2D())); 466 } 467 468 /** 469 * Updates all attributes in this diagram associated with a time event. Ie, 470 * all attributes with track information. 471 * 472 * @return - true if this node has changed state as a result of the time 473 * update 474 */ 475 public boolean updateTime(double curTime) throws SVGException 476 { 477// if (trackManager.getNumTracks() == 0) return false; 478 boolean changeState = super.updateTime(curTime); 479 480 //Get current values for parameters 481 StyleAttribute sty = new StyleAttribute(); 482 boolean shapeChange = false; 483 484 if (getPres(sty.setName("x"))) 485 { 486 float newVal = sty.getFloatValueWithUnits(); 487 if (newVal != x) 488 { 489 x = newVal; 490 shapeChange = true; 491 } 492 } 493 494 if (getPres(sty.setName("y"))) 495 { 496 float newVal = sty.getFloatValueWithUnits(); 497 if (newVal != y) 498 { 499 y = newVal; 500 shapeChange = true; 501 } 502 } 503 504 if (getPres(sty.setName("font-family"))) 505 { 506 String newVal = sty.getStringValue(); 507 if (!newVal.equals(fontFamily)) 508 { 509 fontFamily = newVal; 510 shapeChange = true; 511 } 512 } 513 514 if (getPres(sty.setName("font-size"))) 515 { 516 float newVal = sty.getFloatValueWithUnits(); 517 if (newVal != fontSize) 518 { 519 fontSize = newVal; 520 shapeChange = true; 521 } 522 } 523 524 525 if (getStyle(sty.setName("font-style"))) 526 { 527 String s = sty.getStringValue(); 528 int newVal = fontStyle; 529 if ("normal".equals(s)) 530 { 531 newVal = TXST_NORMAL; 532 } else if ("italic".equals(s)) 533 { 534 newVal = TXST_ITALIC; 535 } else if ("oblique".equals(s)) 536 { 537 newVal = TXST_OBLIQUE; 538 } 539 if (newVal != fontStyle) 540 { 541 fontStyle = newVal; 542 shapeChange = true; 543 } 544 } 545 546 if (getStyle(sty.setName("font-weight"))) 547 { 548 String s = sty.getStringValue(); 549 int newVal = fontWeight; 550 if ("normal".equals(s)) 551 { 552 newVal = TXWE_NORMAL; 553 } else if ("bold".equals(s)) 554 { 555 newVal = TXWE_BOLD; 556 } 557 if (newVal != fontWeight) 558 { 559 fontWeight = newVal; 560 shapeChange = true; 561 } 562 } 563 564 if (shapeChange) 565 { 566 build(); 567// buildFont(); 568// return true; 569 } 570 571 return changeState || shapeChange; 572 } 573}