You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DrawTextParagraph.java 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.sl.draw;
  16. import java.awt.Graphics2D;
  17. import java.awt.Paint;
  18. import java.awt.font.FontRenderContext;
  19. import java.awt.font.LineBreakMeasurer;
  20. import java.awt.font.TextAttribute;
  21. import java.awt.font.TextLayout;
  22. import java.awt.geom.Rectangle2D;
  23. import java.io.InvalidObjectException;
  24. import java.text.AttributedCharacterIterator;
  25. import java.text.AttributedCharacterIterator.Attribute;
  26. import java.text.AttributedString;
  27. import java.util.ArrayList;
  28. import java.util.List;
  29. import java.util.Map;
  30. import org.apache.poi.sl.usermodel.AutoNumberingScheme;
  31. import org.apache.poi.sl.usermodel.Hyperlink;
  32. import org.apache.poi.sl.usermodel.Insets2D;
  33. import org.apache.poi.sl.usermodel.PaintStyle;
  34. import org.apache.poi.sl.usermodel.PlaceableShape;
  35. import org.apache.poi.sl.usermodel.ShapeContainer;
  36. import org.apache.poi.sl.usermodel.TextParagraph;
  37. import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle;
  38. import org.apache.poi.sl.usermodel.TextParagraph.TextAlign;
  39. import org.apache.poi.sl.usermodel.TextRun;
  40. import org.apache.poi.sl.usermodel.TextRun.TextCap;
  41. import org.apache.poi.sl.usermodel.TextShape;
  42. import org.apache.poi.util.StringUtil;
  43. import org.apache.poi.util.Units;
  44. public class DrawTextParagraph implements Drawable {
  45. /** Keys for passing hyperlinks to the graphics context */
  46. public static final XlinkAttribute HYPERLINK_HREF = new XlinkAttribute("href");
  47. public static final XlinkAttribute HYPERLINK_LABEL = new XlinkAttribute("label");
  48. protected TextParagraph<?,?,?> paragraph;
  49. double x, y;
  50. protected List<DrawTextFragment> lines = new ArrayList<DrawTextFragment>();
  51. protected String rawText;
  52. protected DrawTextFragment bullet;
  53. protected int autoNbrIdx = 0;
  54. /**
  55. * the highest line in this paragraph. Used for line spacing.
  56. */
  57. protected double maxLineHeight;
  58. /**
  59. * Defines an attribute used for storing the hyperlink associated with
  60. * some renderable text.
  61. */
  62. private static class XlinkAttribute extends Attribute {
  63. XlinkAttribute(String name) {
  64. super(name);
  65. }
  66. /**
  67. * Resolves instances being deserialized to the predefined constants.
  68. */
  69. protected Object readResolve() throws InvalidObjectException {
  70. if (HYPERLINK_HREF.getName().equals(getName())) {
  71. return HYPERLINK_HREF;
  72. }
  73. if (HYPERLINK_LABEL.getName().equals(getName())) {
  74. return HYPERLINK_LABEL;
  75. }
  76. throw new InvalidObjectException("unknown attribute name");
  77. }
  78. }
  79. public DrawTextParagraph(TextParagraph<?,?,?> paragraph) {
  80. this.paragraph = paragraph;
  81. }
  82. public void setPosition(double x, double y) {
  83. // TODO: replace it, by applyTransform????
  84. this.x = x;
  85. this.y = y;
  86. }
  87. public double getY() {
  88. return y;
  89. }
  90. /**
  91. * Sets the auto numbering index of the handled paragraph
  92. * @param index the auto numbering index
  93. */
  94. public void setAutoNumberingIdx(int index) {
  95. autoNbrIdx = index;
  96. }
  97. public void draw(Graphics2D graphics){
  98. if (lines.isEmpty()) return;
  99. double penY = y;
  100. boolean firstLine = true;
  101. int indentLevel = paragraph.getIndentLevel();
  102. Double leftMargin = paragraph.getLeftMargin();
  103. if (leftMargin == null) {
  104. // if the marL attribute is omitted, then a value of 347663 is implied
  105. leftMargin = Units.toPoints(347663*indentLevel);
  106. }
  107. Double indent = paragraph.getIndent();
  108. if (indent == null) {
  109. indent = Units.toPoints(347663*indentLevel);
  110. }
  111. if (isHSLF()) {
  112. // special handling for HSLF
  113. indent -= leftMargin;
  114. }
  115. // Double rightMargin = paragraph.getRightMargin();
  116. // if (rightMargin == null) {
  117. // rightMargin = 0d;
  118. // }
  119. //The vertical line spacing
  120. Double spacing = paragraph.getLineSpacing();
  121. if (spacing == null) spacing = 100d;
  122. for(DrawTextFragment line : lines){
  123. double penX;
  124. if(firstLine) {
  125. if (!isEmptyParagraph()) {
  126. // TODO: find out character style for empty, but bulleted/numbered lines
  127. bullet = getBullet(graphics, line.getAttributedString().getIterator());
  128. }
  129. if (bullet != null){
  130. bullet.setPosition(x+leftMargin+indent, penY);
  131. bullet.draw(graphics);
  132. // don't let text overlay the bullet and advance by the bullet width
  133. double bulletWidth = bullet.getLayout().getAdvance() + 1;
  134. penX = x + Math.max(leftMargin, leftMargin+indent+bulletWidth);
  135. } else {
  136. penX = x + leftMargin;
  137. }
  138. } else {
  139. penX = x + leftMargin;
  140. }
  141. Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape());
  142. // Insets are already applied on DrawTextShape.drawContent
  143. // but (outer) anchor need to be adjusted
  144. Insets2D insets = paragraph.getParentShape().getInsets();
  145. double leftInset = insets.left;
  146. double rightInset = insets.right;
  147. TextAlign ta = paragraph.getTextAlign();
  148. if (ta == null) ta = TextAlign.LEFT;
  149. switch (ta) {
  150. case CENTER:
  151. penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset - leftMargin) / 2;
  152. break;
  153. case RIGHT:
  154. penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset);
  155. break;
  156. default:
  157. break;
  158. }
  159. line.setPosition(penX, penY);
  160. line.draw(graphics);
  161. if(spacing > 0) {
  162. // If linespacing >= 0, then linespacing is a percentage of normal line height.
  163. penY += spacing*0.01* line.getHeight();
  164. } else {
  165. // negative value means absolute spacing in points
  166. penY += -spacing;
  167. }
  168. firstLine = false;
  169. }
  170. y = penY - y;
  171. }
  172. public float getFirstLineHeight() {
  173. return (lines.isEmpty()) ? 0 : lines.get(0).getHeight();
  174. }
  175. public float getLastLineHeight() {
  176. return (lines.isEmpty()) ? 0 : lines.get(lines.size()-1).getHeight();
  177. }
  178. public boolean isEmptyParagraph() {
  179. return (lines.isEmpty() || rawText.trim().isEmpty());
  180. }
  181. public void applyTransform(Graphics2D graphics) {
  182. }
  183. public void drawContent(Graphics2D graphics) {
  184. }
  185. /**
  186. * break text into lines, each representing a line of text that fits in the wrapping width
  187. *
  188. * @param graphics
  189. */
  190. protected void breakText(Graphics2D graphics){
  191. lines.clear();
  192. DrawFactory fact = DrawFactory.getInstance(graphics);
  193. StringBuilder text = new StringBuilder();
  194. AttributedString at = getAttributedString(graphics, text);
  195. boolean emptyParagraph = ("".equals(text.toString().trim()));
  196. AttributedCharacterIterator it = at.getIterator();
  197. LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());
  198. for (;;) {
  199. int startIndex = measurer.getPosition();
  200. double wrappingWidth = getWrappingWidth(lines.size() == 0, graphics) + 1; // add a pixel to compensate rounding errors
  201. // shape width can be smaller that the sum of insets (this was proved by a test file)
  202. if(wrappingWidth < 0) wrappingWidth = 1;
  203. int nextBreak = text.indexOf("\n", startIndex + 1);
  204. if (nextBreak == -1) nextBreak = it.getEndIndex();
  205. TextLayout layout = measurer.nextLayout((float)wrappingWidth, nextBreak, true);
  206. if (layout == null) {
  207. // layout can be null if the entire word at the current position
  208. // does not fit within the wrapping width. Try with requireNextWord=false.
  209. layout = measurer.nextLayout((float)wrappingWidth, nextBreak, false);
  210. }
  211. if(layout == null) {
  212. // exit if can't break any more
  213. break;
  214. }
  215. int endIndex = measurer.getPosition();
  216. // skip over new line breaks (we paint 'clear' text runs not starting or ending with \n)
  217. if(endIndex < it.getEndIndex() && text.charAt(endIndex) == '\n'){
  218. measurer.setPosition(endIndex + 1);
  219. }
  220. TextAlign hAlign = paragraph.getTextAlign();
  221. if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) {
  222. layout = layout.getJustifiedLayout((float)wrappingWidth);
  223. }
  224. AttributedString str = (emptyParagraph)
  225. ? null // we will not paint empty paragraphs
  226. : new AttributedString(it, startIndex, endIndex);
  227. DrawTextFragment line = fact.getTextFragment(layout, str);
  228. lines.add(line);
  229. maxLineHeight = Math.max(maxLineHeight, line.getHeight());
  230. if(endIndex == it.getEndIndex()) break;
  231. }
  232. rawText = text.toString();
  233. }
  234. protected DrawTextFragment getBullet(Graphics2D graphics, AttributedCharacterIterator firstLineAttr) {
  235. BulletStyle bulletStyle = paragraph.getBulletStyle();
  236. if (bulletStyle == null) return null;
  237. String buCharacter;
  238. AutoNumberingScheme ans = bulletStyle.getAutoNumberingScheme();
  239. if (ans != null) {
  240. buCharacter = ans.format(autoNbrIdx);
  241. } else {
  242. buCharacter = bulletStyle.getBulletCharacter();
  243. }
  244. if (buCharacter == null) return null;
  245. String buFont = bulletStyle.getBulletFont();
  246. if (buFont == null) buFont = paragraph.getDefaultFontFamily();
  247. assert(buFont != null);
  248. PlaceableShape<?,?> ps = getParagraphShape();
  249. PaintStyle fgPaintStyle = bulletStyle.getBulletFontColor();
  250. Paint fgPaint;
  251. if (fgPaintStyle == null) {
  252. fgPaint = (Paint)firstLineAttr.getAttribute(TextAttribute.FOREGROUND);
  253. } else {
  254. fgPaint = new DrawPaint(ps).getPaint(graphics, fgPaintStyle);
  255. }
  256. float fontSize = (Float)firstLineAttr.getAttribute(TextAttribute.SIZE);
  257. Double buSz = bulletStyle.getBulletFontSize();
  258. if (buSz == null) buSz = 100d;
  259. if (buSz > 0) fontSize *= buSz* 0.01;
  260. else fontSize = (float)-buSz;
  261. AttributedString str = new AttributedString(mapFontCharset(buCharacter,buFont));
  262. str.addAttribute(TextAttribute.FOREGROUND, fgPaint);
  263. str.addAttribute(TextAttribute.FAMILY, buFont);
  264. str.addAttribute(TextAttribute.SIZE, fontSize);
  265. TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext());
  266. DrawFactory fact = DrawFactory.getInstance(graphics);
  267. return fact.getTextFragment(layout, str);
  268. }
  269. protected String getRenderableText(TextRun tr) {
  270. StringBuilder buf = new StringBuilder();
  271. TextCap cap = tr.getTextCap();
  272. String tabs = null;
  273. for (char c : tr.getRawText().toCharArray()) {
  274. switch (c) {
  275. case '\t':
  276. if (tabs == null) {
  277. tabs = tab2space(tr);
  278. }
  279. buf.append(tabs);
  280. break;
  281. case '\u000b':
  282. buf.append('\n');
  283. break;
  284. default:
  285. switch (cap) {
  286. case ALL: c = Character.toUpperCase(c); break;
  287. case SMALL: c = Character.toLowerCase(c); break;
  288. case NONE: break;
  289. }
  290. buf.append(c);
  291. break;
  292. }
  293. }
  294. return buf.toString();
  295. }
  296. /**
  297. * Replace a tab with the effective number of white spaces.
  298. */
  299. private String tab2space(TextRun tr) {
  300. AttributedString string = new AttributedString(" ");
  301. String fontFamily = tr.getFontFamily();
  302. if (fontFamily == null) fontFamily = "Lucida Sans";
  303. string.addAttribute(TextAttribute.FAMILY, fontFamily);
  304. Double fs = tr.getFontSize();
  305. if (fs == null) fs = 12d;
  306. string.addAttribute(TextAttribute.SIZE, fs.floatValue());
  307. TextLayout l = new TextLayout(string.getIterator(), new FontRenderContext(null, true, true));
  308. double wspace = l.getAdvance();
  309. Double tabSz = paragraph.getDefaultTabSize();
  310. if (tabSz == null) tabSz = wspace*4;
  311. int numSpaces = (int)Math.ceil(tabSz / wspace);
  312. StringBuilder buf = new StringBuilder();
  313. for(int i = 0; i < numSpaces; i++) {
  314. buf.append(' ');
  315. }
  316. return buf.toString();
  317. }
  318. /**
  319. * Returns wrapping width to break lines in this paragraph
  320. *
  321. * @param firstLine whether the first line is breaking
  322. *
  323. * @return wrapping width in points
  324. */
  325. protected double getWrappingWidth(boolean firstLine, Graphics2D graphics){
  326. // internal margins for the text box
  327. Insets2D insets = paragraph.getParentShape().getInsets();
  328. double leftInset = insets.left;
  329. double rightInset = insets.right;
  330. Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape());
  331. int indentLevel = paragraph.getIndentLevel();
  332. if (indentLevel == -1) {
  333. // default to 0, if indentLevel is not set
  334. indentLevel = 0;
  335. }
  336. Double leftMargin = paragraph.getLeftMargin();
  337. if (leftMargin == null) {
  338. // if the marL attribute is omitted, then a value of 347663 is implied
  339. leftMargin = Units.toPoints(347663L*(indentLevel+1));
  340. }
  341. Double indent = paragraph.getIndent();
  342. if (indent == null) {
  343. indent = Units.toPoints(347663L*indentLevel);
  344. }
  345. Double rightMargin = paragraph.getRightMargin();
  346. if (rightMargin == null) {
  347. rightMargin = 0d;
  348. }
  349. double width;
  350. TextShape<?,?> ts = paragraph.getParentShape();
  351. if (!ts.getWordWrap()) {
  352. // if wordWrap == false then we return the advance to the right border of the sheet
  353. width = ts.getSheet().getSlideShow().getPageSize().getWidth() - anchor.getX();
  354. } else {
  355. width = anchor.getWidth() - leftInset - rightInset - leftMargin - rightMargin;
  356. if (firstLine && !isHSLF()) {
  357. if (bullet != null){
  358. if (indent > 0) width -= indent;
  359. } else {
  360. if (indent > 0) width -= indent; // first line indentation
  361. else if (indent < 0) { // hanging indentation: the first line start at the left margin
  362. width += leftMargin;
  363. }
  364. }
  365. }
  366. }
  367. return width;
  368. }
  369. private static class AttributedStringData {
  370. Attribute attribute;
  371. Object value;
  372. int beginIndex, endIndex;
  373. AttributedStringData(Attribute attribute, Object value, int beginIndex, int endIndex) {
  374. this.attribute = attribute;
  375. this.value = value;
  376. this.beginIndex = beginIndex;
  377. this.endIndex = endIndex;
  378. }
  379. }
  380. /**
  381. * Helper method for paint style relative to bounds, e.g. gradient paint
  382. */
  383. @SuppressWarnings("rawtypes")
  384. private PlaceableShape<?,?> getParagraphShape() {
  385. PlaceableShape<?,?> ps = new PlaceableShape(){
  386. public ShapeContainer<?,?> getParent() { return null; }
  387. public Rectangle2D getAnchor() { return paragraph.getParentShape().getAnchor(); }
  388. public void setAnchor(Rectangle2D anchor) {}
  389. public double getRotation() { return 0; }
  390. public void setRotation(double theta) {}
  391. public void setFlipHorizontal(boolean flip) {}
  392. public void setFlipVertical(boolean flip) {}
  393. public boolean getFlipHorizontal() { return false; }
  394. public boolean getFlipVertical() { return false; }
  395. };
  396. return ps;
  397. }
  398. protected AttributedString getAttributedString(Graphics2D graphics, StringBuilder text){
  399. List<AttributedStringData> attList = new ArrayList<AttributedStringData>();
  400. if (text == null) text = new StringBuilder();
  401. PlaceableShape<?,?> ps = getParagraphShape();
  402. DrawFontManager fontHandler = (DrawFontManager)graphics.getRenderingHint(Drawable.FONT_HANDLER);
  403. for (TextRun run : paragraph){
  404. String runText = getRenderableText(run);
  405. // skip empty runs
  406. if (runText.isEmpty()) continue;
  407. // user can pass an custom object to convert fonts
  408. String fontFamily = run.getFontFamily();
  409. @SuppressWarnings("unchecked")
  410. Map<String,String> fontMap = (Map<String,String>)graphics.getRenderingHint(Drawable.FONT_MAP);
  411. if (fontMap != null && fontMap.containsKey(fontFamily)) {
  412. fontFamily = fontMap.get(fontFamily);
  413. }
  414. if(fontHandler != null) {
  415. fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily());
  416. }
  417. if (fontFamily == null) {
  418. fontFamily = paragraph.getDefaultFontFamily();
  419. }
  420. int beginIndex = text.length();
  421. text.append(mapFontCharset(runText,fontFamily));
  422. int endIndex = text.length();
  423. attList.add(new AttributedStringData(TextAttribute.FAMILY, fontFamily, beginIndex, endIndex));
  424. PaintStyle fgPaintStyle = run.getFontColor();
  425. Paint fgPaint = new DrawPaint(ps).getPaint(graphics, fgPaintStyle);
  426. attList.add(new AttributedStringData(TextAttribute.FOREGROUND, fgPaint, beginIndex, endIndex));
  427. Double fontSz = run.getFontSize();
  428. if (fontSz == null) fontSz = paragraph.getDefaultFontSize();
  429. attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), beginIndex, endIndex));
  430. if(run.isBold()) {
  431. attList.add(new AttributedStringData(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, beginIndex, endIndex));
  432. }
  433. if(run.isItalic()) {
  434. attList.add(new AttributedStringData(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, beginIndex, endIndex));
  435. }
  436. if(run.isUnderlined()) {
  437. attList.add(new AttributedStringData(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, beginIndex, endIndex));
  438. attList.add(new AttributedStringData(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL, beginIndex, endIndex));
  439. }
  440. if(run.isStrikethrough()) {
  441. attList.add(new AttributedStringData(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, beginIndex, endIndex));
  442. }
  443. if(run.isSubscript()) {
  444. attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB, beginIndex, endIndex));
  445. }
  446. if(run.isSuperscript()) {
  447. attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, beginIndex, endIndex));
  448. }
  449. Hyperlink hl = run.getHyperlink();
  450. if (hl != null) {
  451. attList.add(new AttributedStringData(HYPERLINK_HREF, hl.getAddress(), beginIndex, endIndex));
  452. attList.add(new AttributedStringData(HYPERLINK_LABEL, hl.getLabel(), beginIndex, endIndex));
  453. }
  454. }
  455. // ensure that the paragraph contains at least one character
  456. // We need this trick to correctly measure text
  457. if (text.length() == 0) {
  458. Double fontSz = paragraph.getDefaultFontSize();
  459. text.append(" ");
  460. attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), 0, 1));
  461. }
  462. AttributedString string = new AttributedString(text.toString());
  463. for (AttributedStringData asd : attList) {
  464. string.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
  465. }
  466. return string;
  467. }
  468. protected boolean isHSLF() {
  469. return paragraph.getClass().getName().contains("HSLF");
  470. }
  471. /**
  472. * Map text charset depending on font family.
  473. * Currently this only maps for wingdings font (into unicode private use area)
  474. *
  475. * @param text the raw text
  476. * @param fontFamily the font family
  477. * @return AttributedString with mapped codepoints
  478. *
  479. * @see <a href="http://stackoverflow.com/questions/8692095">Drawing exotic fonts in a java applet</a>
  480. * @see StringUtil#mapMsCodepointString(String)
  481. */
  482. protected String mapFontCharset(String text, String fontFamily) {
  483. // TODO: find a real charset mapping solution instead of hard coding for Wingdings
  484. String attStr = text;
  485. if ("Wingdings".equalsIgnoreCase(fontFamily)) {
  486. // wingdings doesn't contain high-surrogates, so chars are ok
  487. boolean changed = false;
  488. char chrs[] = attStr.toCharArray();
  489. for (int i=0; i<chrs.length; i++) {
  490. // only change valid chars
  491. if ((0x20 <= chrs[i] && chrs[i] <= 0x7f) ||
  492. (0xa0 <= chrs[i] && chrs[i] <= 0xff)) {
  493. chrs[i] |= 0xf000;
  494. changed = true;
  495. }
  496. }
  497. if (changed) {
  498. attStr = new String(chrs);
  499. }
  500. }
  501. return attStr;
  502. }
  503. }