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.util.FontSystem; 039import com.kitfox.svg.xml.StyleAttribute; 040import java.awt.Graphics2D; 041import java.awt.Shape; 042import java.awt.geom.AffineTransform; 043import java.awt.geom.GeneralPath; 044import java.awt.geom.Point2D; 045import java.awt.geom.Rectangle2D; 046import java.io.Serializable; 047import java.util.LinkedList; 048import java.util.List; 049import java.util.logging.Level; 050import java.util.logging.Logger; 051import java.util.regex.Matcher; 052import java.util.regex.Pattern; 053 054/** 055 * @author Mark McKay 056 * @author <a href="mailto:mark@kitfox.com">Mark McKay</a> 057 */ 058public class Text extends ShapeElement 059{ 060 public static final String TAG_NAME = "text"; 061 062 float x = 0; 063 float y = 0; 064 AffineTransform transform = null; 065 String fontFamily; 066 float fontSize; 067 //List of strings and tspans containing the content of this node 068 LinkedList<Serializable> content = new LinkedList<Serializable>(); 069 Shape textShape; 070 public static final int TXAN_START = 0; 071 public static final int TXAN_MIDDLE = 1; 072 public static final int TXAN_END = 2; 073 int textAnchor = TXAN_START; 074 public static final int TXST_NORMAL = 0; 075 public static final int TXST_ITALIC = 1; 076 public static final int TXST_OBLIQUE = 2; 077 int fontStyle; 078 public static final int TXWE_NORMAL = 0; 079 public static final int TXWE_BOLD = 1; 080 public static final int TXWE_BOLDER = 2; 081 public static final int TXWE_LIGHTER = 3; 082 public static final int TXWE_100 = 4; 083 public static final int TXWE_200 = 5; 084 public static final int TXWE_300 = 6; 085 public static final int TXWE_400 = 7; 086 public static final int TXWE_500 = 8; 087 public static final int TXWE_600 = 9; 088 public static final int TXWE_700 = 10; 089 public static final int TXWE_800 = 11; 090 public static final int TXWE_900 = 12; 091 int fontWeight; 092 093 float textLength = -1; 094 String lengthAdjust = "spacing"; 095 096 /** 097 * Creates a new instance of Stop 098 */ 099 public Text() 100 { 101 } 102 103 @Override 104 public String getTagName() 105 { 106 return TAG_NAME; 107 } 108 109 public void appendText(String text) 110 { 111 content.addLast(text); 112 } 113 114 public void appendTspan(Tspan tspan) throws SVGElementException 115 { 116 super.loaderAddChild(null, tspan); 117 content.addLast(tspan); 118 } 119 120 /** 121 * Discard cached information 122 */ 123 public void rebuild() throws SVGException 124 { 125 build(); 126 } 127 128 public List<Serializable> getContent() 129 { 130 return content; 131 } 132 133 /** 134 * Called after the start element but before the end element to indicate 135 * each child tag that has been processed 136 */ 137 @Override 138 public void loaderAddChild(SVGLoaderHelper helper, SVGElement child) throws SVGElementException 139 { 140 super.loaderAddChild(helper, child); 141 142 content.addLast(child); 143 } 144 145 /** 146 * Called during load process to add text scanned within a tag 147 */ 148 @Override 149 public void loaderAddText(SVGLoaderHelper helper, String text) 150 { 151 Matcher matchWs = Pattern.compile("\\s*").matcher(text); 152 if (!matchWs.matches()) 153 { 154 content.addLast(text); 155 } 156 } 157 158 @Override 159 public void build() throws SVGException 160 { 161 super.build(); 162 163 StyleAttribute sty = new StyleAttribute(); 164 165 if (getPres(sty.setName("x"))) 166 { 167 x = sty.getFloatValueWithUnits(); 168 } 169 170 if (getPres(sty.setName("y"))) 171 { 172 y = sty.getFloatValueWithUnits(); 173 } 174 175 if (getStyle(sty.setName("font-family"))) 176 { 177 fontFamily = sty.getStringValue(); 178 } 179 else 180 { 181 fontFamily = "SansSerif"; 182 } 183 184 if (getStyle(sty.setName("font-size"))) 185 { 186 fontSize = sty.getFloatValueWithUnits(); 187 } 188 else 189 { 190 fontSize = 12f; 191 } 192 193 if (getStyle(sty.setName("textLength"))) 194 { 195 textLength = sty.getFloatValueWithUnits(); 196 } 197 else 198 { 199 textLength = -1; 200 } 201 202 if (getStyle(sty.setName("lengthAdjust"))) 203 { 204 lengthAdjust = sty.getStringValue(); 205 } 206 else 207 { 208 lengthAdjust = "spacing"; 209 } 210 211 if (getStyle(sty.setName("font-style"))) 212 { 213 String s = sty.getStringValue(); 214 if ("normal".equals(s)) 215 { 216 fontStyle = TXST_NORMAL; 217 } else if ("italic".equals(s)) 218 { 219 fontStyle = TXST_ITALIC; 220 } else if ("oblique".equals(s)) 221 { 222 fontStyle = TXST_OBLIQUE; 223 } 224 } else 225 { 226 fontStyle = TXST_NORMAL; 227 } 228 229 if (getStyle(sty.setName("font-weight"))) 230 { 231 String s = sty.getStringValue(); 232 if ("normal".equals(s)) 233 { 234 fontWeight = TXWE_NORMAL; 235 } else if ("bold".equals(s)) 236 { 237 fontWeight = TXWE_BOLD; 238 } 239 } else 240 { 241 fontWeight = TXWE_NORMAL; 242 } 243 244 if (getStyle(sty.setName("text-anchor"))) 245 { 246 String s = sty.getStringValue(); 247 if (s.equals("middle")) 248 { 249 textAnchor = TXAN_MIDDLE; 250 } else if (s.equals("end")) 251 { 252 textAnchor = TXAN_END; 253 } else 254 { 255 textAnchor = TXAN_START; 256 } 257 } else 258 { 259 textAnchor = TXAN_START; 260 } 261 262 //text anchor 263 //text-decoration 264 //text-rendering 265 266 buildText(); 267 } 268 269 protected void buildText() throws SVGException 270 { 271 //Get font 272 String[] families = fontFamily.split(","); 273 Font font = null; 274 for (int i = 0; i < families.length; ++i) 275 { 276 font = diagram.getUniverse().getFont(families[i]); 277 if (font != null) 278 { 279 break; 280 } 281 } 282 283 if (font == null) 284 { 285 //Check system fonts 286 font = FontSystem.createFont(fontFamily, fontStyle, fontWeight, (int)fontSize); 287 } 288 289 if (font == null) 290 { 291 Logger.getLogger(Text.class.getName()).log(Level.WARNING, "Could not create font " + fontFamily); 292 font = FontSystem.createFont("Serif", fontStyle, fontWeight, fontStyle); 293 } 294 295 GeneralPath textPath = new GeneralPath(); 296 textShape = textPath; 297 298 float cursorX = x, cursorY = y; 299 300 301 AffineTransform xform = new AffineTransform(); 302 303 for (Serializable obj : content) { 304 if (obj instanceof String) 305 { 306 String text = (String) obj; 307 if (text != null) 308 { 309 text = text.trim(); 310 } 311 312// strokeWidthScalar = 1f / fontScale; 313 314 for (int i = 0; i < text.length(); i++) 315 { 316 xform.setToIdentity(); 317 xform.setToTranslation(cursorX, cursorY); 318// xform.scale(fontScale, fontScale); 319// g.transform(xform); 320 321 String unicode = text.substring(i, i + 1); 322 MissingGlyph glyph = font.getGlyph(unicode); 323 324 Shape path = glyph.getPath(); 325 if (path != null) 326 { 327 path = xform.createTransformedShape(path); 328 textPath.append(path, false); 329 } 330// else glyph.render(g); 331 332// cursorX += fontScale * glyph.getHorizAdvX(); 333 cursorX += glyph.getHorizAdvX(); 334 335// g.setTransform(oldXform); 336 } 337 338 strokeWidthScalar = 1f; 339 } 340 else if (obj instanceof Tspan) 341 { 342// Tspan tspan = (Tspan) obj; 343// 344// xform.setToIdentity(); 345// xform.setToTranslation(cursorX, cursorY); 346// xform.scale(fontScale, fontScale); 347//// tspan.setCursorX(cursorX); 348//// tspan.setCursorY(cursorY); 349// 350// Shape tspanShape = tspan.getShape(); 351// tspanShape = xform.createTransformedShape(tspanShape); 352// textPath.append(tspanShape, false); 353//// tspan.render(g); 354//// cursorX = tspan.getCursorX(); 355//// cursorY = tspan.getCursorY(); 356 357 358 Tspan tspan = (Tspan)obj; 359 Point2D cursor = new Point2D.Float(cursorX, cursorY); 360// tspan.setCursorX(cursorX); 361// tspan.setCursorY(cursorY); 362 tspan.appendToShape(textPath, cursor); 363// cursorX = tspan.getCursorX(); 364// cursorY = tspan.getCursorY(); 365 cursorX = (float)cursor.getX(); 366 cursorY = (float)cursor.getY(); 367 368 } 369 370 } 371 372 switch (textAnchor) 373 { 374 case TXAN_MIDDLE: 375 { 376 AffineTransform at = new AffineTransform(); 377 at.translate(-textPath.getBounds().getWidth() / 2, 0); 378 textPath.transform(at); 379 break; 380 } 381 case TXAN_END: 382 { 383 AffineTransform at = new AffineTransform(); 384 at.translate(-textPath.getBounds().getWidth(), 0); 385 textPath.transform(at); 386 break; 387 } 388 } 389 } 390 391// private void buildSysFont(java.awt.Font font) throws SVGException 392// { 393// GeneralPath textPath = new GeneralPath(); 394// textShape = textPath; 395// 396// float cursorX = x, cursorY = y; 397// 398//// FontMetrics fm = g.getFontMetrics(font); 399// FontRenderContext frc = new FontRenderContext(null, true, true); 400// 401//// FontFace fontFace = font.getFontFace(); 402// //int unitsPerEm = fontFace.getUnitsPerEm(); 403//// int ascent = fm.getAscent(); 404//// float fontScale = fontSize / (float)ascent; 405// 406//// AffineTransform oldXform = g.getTransform(); 407// AffineTransform xform = new AffineTransform(); 408// 409// for (Iterator it = content.iterator(); it.hasNext();) 410// { 411// Object obj = it.next(); 412// 413// if (obj instanceof String) 414// { 415// String text = (String)obj; 416// text = text.trim(); 417// 418// Shape textShape = font.createGlyphVector(frc, text).getOutline(cursorX, cursorY); 419// textPath.append(textShape, false); 420//// renderShape(g, textShape); 421//// g.drawString(text, cursorX, cursorY); 422// 423// Rectangle2D rect = font.getStringBounds(text, frc); 424// cursorX += (float) rect.getWidth(); 425// } else if (obj instanceof Tspan) 426// { 427// /* 428// Tspan tspan = (Tspan)obj; 429// 430// xform.setToIdentity(); 431// xform.setToTranslation(cursorX, cursorY); 432// 433// Shape tspanShape = tspan.getShape(); 434// tspanShape = xform.createTransformedShape(tspanShape); 435// textArea.add(new Area(tspanShape)); 436// 437// cursorX += tspanShape.getBounds2D().getWidth(); 438// */ 439// 440// 441// Tspan tspan = (Tspan)obj; 442// Point2D cursor = new Point2D.Float(cursorX, cursorY); 443//// tspan.setCursorX(cursorX); 444//// tspan.setCursorY(cursorY); 445// tspan.appendToShape(textPath, cursor); 446//// cursorX = tspan.getCursorX(); 447//// cursorY = tspan.getCursorY(); 448// cursorX = (float)cursor.getX(); 449// cursorY = (float)cursor.getY(); 450// 451// } 452// } 453// 454// switch (textAnchor) 455// { 456// case TXAN_MIDDLE: 457// { 458// AffineTransform at = new AffineTransform(); 459// at.translate(-textPath.getBounds().getWidth() / 2, 0); 460// textPath.transform(at); 461// break; 462// } 463// case TXAN_END: 464// { 465// AffineTransform at = new AffineTransform(); 466// at.translate(-Math.ceil(textPath.getBounds().getWidth()), 0); 467// textPath.transform(at); 468// break; 469// } 470// } 471// } 472 473 @Override 474 public void render(Graphics2D g) throws SVGException 475 { 476 beginLayer(g); 477 renderShape(g, textShape); 478 finishLayer(g); 479 } 480 481 @Override 482 public Shape getShape() 483 { 484 return shapeToParent(textShape); 485 } 486 487 @Override 488 public Rectangle2D getBoundingBox() throws SVGException 489 { 490 return boundsToParent(includeStrokeInBounds(textShape.getBounds2D())); 491 } 492 493 /** 494 * Updates all attributes in this diagram associated with a time event. Ie, 495 * all attributes with track information. 496 * 497 * @return - true if this node has changed state as a result of the time 498 * update 499 */ 500 @Override 501 public boolean updateTime(double curTime) throws SVGException 502 { 503// if (trackManager.getNumTracks() == 0) return false; 504 boolean changeState = super.updateTime(curTime); 505 506 //Get current values for parameters 507 StyleAttribute sty = new StyleAttribute(); 508 boolean shapeChange = false; 509 510 if (getPres(sty.setName("x"))) 511 { 512 float newVal = sty.getFloatValueWithUnits(); 513 if (newVal != x) 514 { 515 x = newVal; 516 shapeChange = true; 517 } 518 } 519 520 if (getPres(sty.setName("y"))) 521 { 522 float newVal = sty.getFloatValueWithUnits(); 523 if (newVal != y) 524 { 525 y = newVal; 526 shapeChange = true; 527 } 528 } 529 530 if (getStyle(sty.setName("textLength"))) 531 { 532 textLength = sty.getFloatValueWithUnits(); 533 } 534 else 535 { 536 textLength = -1; 537 } 538 539 if (getStyle(sty.setName("lengthAdjust"))) 540 { 541 lengthAdjust = sty.getStringValue(); 542 } 543 else 544 { 545 lengthAdjust = "spacing"; 546 } 547 548 if (getPres(sty.setName("font-family"))) 549 { 550 String newVal = sty.getStringValue(); 551 if (!newVal.equals(fontFamily)) 552 { 553 fontFamily = newVal; 554 shapeChange = true; 555 } 556 } 557 558 if (getPres(sty.setName("font-size"))) 559 { 560 float newVal = sty.getFloatValueWithUnits(); 561 if (newVal != fontSize) 562 { 563 fontSize = newVal; 564 shapeChange = true; 565 } 566 } 567 568 569 if (getStyle(sty.setName("font-style"))) 570 { 571 String s = sty.getStringValue(); 572 int newVal = fontStyle; 573 if ("normal".equals(s)) 574 { 575 newVal = TXST_NORMAL; 576 } else if ("italic".equals(s)) 577 { 578 newVal = TXST_ITALIC; 579 } else if ("oblique".equals(s)) 580 { 581 newVal = TXST_OBLIQUE; 582 } 583 if (newVal != fontStyle) 584 { 585 fontStyle = newVal; 586 shapeChange = true; 587 } 588 } 589 590 if (getStyle(sty.setName("font-weight"))) 591 { 592 String s = sty.getStringValue(); 593 int newVal = fontWeight; 594 if ("normal".equals(s)) 595 { 596 newVal = TXWE_NORMAL; 597 } else if ("bold".equals(s)) 598 { 599 newVal = TXWE_BOLD; 600 } 601 if (newVal != fontWeight) 602 { 603 fontWeight = newVal; 604 shapeChange = true; 605 } 606 } 607 608 if (shapeChange) 609 { 610 build(); 611// buildFont(); 612// return true; 613 } 614 615 return changeState || shapeChange; 616 } 617}