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 29KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  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 static org.apache.logging.log4j.util.Unbox.box;
  17. import java.awt.Dimension;
  18. import java.awt.Font;
  19. import java.awt.Graphics2D;
  20. import java.awt.Paint;
  21. import java.awt.font.FontRenderContext;
  22. import java.awt.font.LineBreakMeasurer;
  23. import java.awt.font.TextAttribute;
  24. import java.awt.font.TextLayout;
  25. import java.awt.geom.Rectangle2D;
  26. import java.io.InvalidObjectException;
  27. import java.text.AttributedCharacterIterator;
  28. import java.text.AttributedCharacterIterator.Attribute;
  29. import java.text.AttributedString;
  30. import java.time.LocalDateTime;
  31. import java.util.ArrayList;
  32. import java.util.Arrays;
  33. import java.util.Calendar;
  34. import java.util.HashMap;
  35. import java.util.List;
  36. import java.util.Locale;
  37. import java.util.Map;
  38. import org.apache.logging.log4j.LogManager;
  39. import org.apache.logging.log4j.Logger;
  40. import org.apache.poi.common.usermodel.fonts.FontGroup;
  41. import org.apache.poi.common.usermodel.fonts.FontGroup.FontGroupRange;
  42. import org.apache.poi.common.usermodel.fonts.FontInfo;
  43. import org.apache.poi.sl.usermodel.AutoNumberingScheme;
  44. import org.apache.poi.sl.usermodel.Hyperlink;
  45. import org.apache.poi.sl.usermodel.Insets2D;
  46. import org.apache.poi.sl.usermodel.PaintStyle;
  47. import org.apache.poi.sl.usermodel.PlaceableShape;
  48. import org.apache.poi.sl.usermodel.PlaceholderDetails;
  49. import org.apache.poi.sl.usermodel.SimpleShape;
  50. import org.apache.poi.sl.usermodel.Slide;
  51. import org.apache.poi.sl.usermodel.TextParagraph;
  52. import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle;
  53. import org.apache.poi.sl.usermodel.TextParagraph.TextAlign;
  54. import org.apache.poi.sl.usermodel.TextRun;
  55. import org.apache.poi.sl.usermodel.TextRun.FieldType;
  56. import org.apache.poi.sl.usermodel.TextShape;
  57. import org.apache.poi.sl.usermodel.TextShape.TextDirection;
  58. import org.apache.poi.util.Internal;
  59. import org.apache.poi.util.LocaleUtil;
  60. import org.apache.poi.util.Units;
  61. public class DrawTextParagraph implements Drawable {
  62. private static final Logger LOG = LogManager.getLogger(DrawTextParagraph.class);
  63. /** Keys for passing hyperlinks to the graphics context */
  64. public static final XlinkAttribute HYPERLINK_HREF = new XlinkAttribute("href");
  65. public static final XlinkAttribute HYPERLINK_LABEL = new XlinkAttribute("label");
  66. protected TextParagraph<?,?,?> paragraph;
  67. double x, y;
  68. protected List<DrawTextFragment> lines = new ArrayList<>();
  69. protected String rawText;
  70. protected DrawTextFragment bullet;
  71. protected int autoNbrIdx;
  72. protected boolean firstParagraph = true;
  73. /**
  74. * Defines an attribute used for storing the hyperlink associated with
  75. * some renderable text.
  76. */
  77. private static class XlinkAttribute extends Attribute {
  78. XlinkAttribute(String name) {
  79. super(name);
  80. }
  81. /**
  82. * Resolves instances being deserialized to the predefined constants.
  83. */
  84. @Override
  85. protected Object readResolve() throws InvalidObjectException {
  86. if (HYPERLINK_HREF.getName().equals(getName())) {
  87. return HYPERLINK_HREF;
  88. }
  89. if (HYPERLINK_LABEL.getName().equals(getName())) {
  90. return HYPERLINK_LABEL;
  91. }
  92. throw new InvalidObjectException("unknown attribute name");
  93. }
  94. }
  95. public DrawTextParagraph(TextParagraph<?,?,?> paragraph) {
  96. this.paragraph = paragraph;
  97. }
  98. public void setPosition(double x, double y) {
  99. // TODO: replace it, by applyTransform????
  100. this.x = x;
  101. this.y = y;
  102. }
  103. public double getY() {
  104. return y;
  105. }
  106. /**
  107. * Sets the auto numbering index of the handled paragraph
  108. * @param index the auto numbering index
  109. */
  110. public void setAutoNumberingIdx(int index) {
  111. autoNbrIdx = index;
  112. }
  113. @Override
  114. public void draw(Graphics2D graphics){
  115. if (lines.isEmpty()) {
  116. return;
  117. }
  118. final boolean isHSLF = isHSLF();
  119. double penY = y;
  120. int indentLevel = paragraph.getIndentLevel();
  121. Double leftMargin = paragraph.getLeftMargin();
  122. if (leftMargin == null) {
  123. // if the marL attribute is omitted, then a value of 347663 is implied
  124. leftMargin = Units.toPoints(347663L*indentLevel);
  125. }
  126. Double indent = paragraph.getIndent();
  127. if (indent == null) {
  128. indent = Units.toPoints(347663L*indentLevel);
  129. }
  130. // Double rightMargin = paragraph.getRightMargin();
  131. // if (rightMargin == null) {
  132. // rightMargin = 0d;
  133. // }
  134. //The vertical line spacing
  135. Double spacing = paragraph.getLineSpacing();
  136. if (spacing == null) {
  137. spacing = 100d;
  138. }
  139. DrawTextFragment lastLine = null;
  140. for(DrawTextFragment line : lines){
  141. double penX;
  142. if (!(isFirstParagraph() && lastLine == null)) {
  143. // penY is now on descent line of the last text fragment
  144. // need to substract descent height to get back to the baseline of the last fragment
  145. // then add a multiple of the line height of the current text height
  146. penY -= line.getLeading() + ((lastLine == null) ? 0 : lastLine.getLayout().getDescent());
  147. if(spacing > 0) {
  148. // If linespacing >= 0, then linespacing is a percentage of normal line height.
  149. penY += (spacing*0.01) * line.getHeight(); // + (isHSLF ? line.getLayout().getLeading() : 0));
  150. } else {
  151. // negative value means absolute spacing in points
  152. penY += -spacing;
  153. }
  154. penY -= line.getLayout().getAscent();
  155. }
  156. penX = x + leftMargin;
  157. if (lastLine == null) {
  158. if (!isEmptyParagraph()) {
  159. // TODO: find out character style for empty, but bulleted/numbered lines
  160. bullet = getBullet(graphics, line.getAttributedString().getIterator());
  161. }
  162. if (bullet != null) {
  163. bullet.setPosition(isHSLF ? x+indent : x+leftMargin+indent, penY);
  164. bullet.draw(graphics);
  165. // don't let text overlay the bullet and advance by the bullet width
  166. double bulletWidth = bullet.getLayout().getAdvance() + 1;
  167. penX = x + (isHSLF ? leftMargin : Math.max(leftMargin, leftMargin+indent+bulletWidth));
  168. }
  169. }
  170. Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape());
  171. // Insets are already applied on DrawTextShape.drawContent
  172. // but (outer) anchor need to be adjusted
  173. Insets2D insets = paragraph.getParentShape().getInsets();
  174. double leftInset = insets.left;
  175. double rightInset = insets.right;
  176. TextAlign ta = paragraph.getTextAlign();
  177. if (ta == null) {
  178. ta = TextAlign.LEFT;
  179. }
  180. switch (ta) {
  181. case CENTER:
  182. penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset - leftMargin) / 2;
  183. break;
  184. case RIGHT:
  185. penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset);
  186. break;
  187. default:
  188. break;
  189. }
  190. line.setPosition(penX, penY);
  191. line.draw(graphics);
  192. penY += line.getHeight();
  193. lastLine = line;
  194. }
  195. y = penY - y;
  196. }
  197. public float getFirstLineLeading() {
  198. return (lines.isEmpty()) ? 0 : lines.get(0).getLeading();
  199. }
  200. public float getFirstLineHeight() {
  201. return (lines.isEmpty()) ? 0 : lines.get(0).getHeight();
  202. }
  203. public float getLastLineHeight() {
  204. return (lines.isEmpty()) ? 0 : lines.get(lines.size()-1).getHeight();
  205. }
  206. public boolean isEmptyParagraph() {
  207. return (lines.isEmpty() || rawText.trim().isEmpty());
  208. }
  209. @Override
  210. public void applyTransform(Graphics2D graphics) {
  211. }
  212. @Override
  213. public void drawContent(Graphics2D graphics) {
  214. }
  215. /**
  216. * break text into lines, each representing a line of text that fits in the wrapping width
  217. *
  218. * @param graphics The drawing context for computing text-lengths.
  219. */
  220. protected void breakText(Graphics2D graphics){
  221. lines.clear();
  222. DrawFactory fact = DrawFactory.getInstance(graphics);
  223. StringBuilder text = new StringBuilder();
  224. List<AttributedStringData> attList = getAttributedString(graphics, text);
  225. AttributedString as = new AttributedString(text.toString());
  226. AttributedString asNoCR = new AttributedString(text.toString().replaceAll("[\\r\\n]", " "));
  227. for (AttributedStringData asd : attList) {
  228. as.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
  229. asNoCR.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
  230. }
  231. AttributedCharacterIterator it = as.getIterator();
  232. AttributedCharacterIterator itNoCR = asNoCR.getIterator();
  233. LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());
  234. for (;;) {
  235. int startIndex = measurer.getPosition();
  236. // add a pixel to compensate rounding errors
  237. double wrappingWidth = getWrappingWidth(lines.isEmpty(), graphics) + 1;
  238. // shape width can be smaller that the sum of insets (this was proved by a test file)
  239. if(wrappingWidth < 0) {
  240. wrappingWidth = 1;
  241. }
  242. // usually "\n" is added after a line, if it occurs before it - only possible as first char -
  243. // we need to add an empty line
  244. TextLayout layout;
  245. int endIndex;
  246. if (startIndex == 0 && text.toString().startsWith("\n")) {
  247. layout = measurer.nextLayout((float) wrappingWidth, 1, false);
  248. endIndex = 1;
  249. } else {
  250. int nextBreak = text.indexOf("\n", startIndex + 1);
  251. if (nextBreak == -1) {
  252. nextBreak = it.getEndIndex();
  253. }
  254. layout = measurer.nextLayout((float) wrappingWidth, nextBreak, true);
  255. if (layout == null) {
  256. // layout can be null if the entire word at the current position
  257. // does not fit within the wrapping width. Try with requireNextWord=false.
  258. layout = measurer.nextLayout((float) wrappingWidth, nextBreak, false);
  259. }
  260. if (layout == null) {
  261. // exit if can't break any more
  262. break;
  263. }
  264. endIndex = measurer.getPosition();
  265. // skip over new line breaks (we paint 'clear' text runs not starting or ending with \n)
  266. if (endIndex < it.getEndIndex() && text.charAt(endIndex) == '\n') {
  267. measurer.setPosition(endIndex + 1);
  268. }
  269. TextAlign hAlign = paragraph.getTextAlign();
  270. if (hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) {
  271. layout = layout.getJustifiedLayout((float) wrappingWidth);
  272. }
  273. }
  274. AttributedString str = new AttributedString(itNoCR, startIndex, endIndex);
  275. DrawTextFragment line = fact.getTextFragment(layout, str);
  276. lines.add(line);
  277. if(endIndex == it.getEndIndex()) {
  278. break;
  279. }
  280. }
  281. rawText = text.toString();
  282. }
  283. protected DrawTextFragment getBullet(Graphics2D graphics, AttributedCharacterIterator firstLineAttr) {
  284. BulletStyle bulletStyle = paragraph.getBulletStyle();
  285. if (bulletStyle == null) {
  286. return null;
  287. }
  288. String buCharacter;
  289. AutoNumberingScheme ans = bulletStyle.getAutoNumberingScheme();
  290. if (ans != null) {
  291. buCharacter = ans.format(autoNbrIdx);
  292. } else {
  293. buCharacter = bulletStyle.getBulletCharacter();
  294. }
  295. if (buCharacter == null) {
  296. return null;
  297. }
  298. PlaceableShape<?,?> ps = getParagraphShape();
  299. PaintStyle fgPaintStyle = bulletStyle.getBulletFontColor();
  300. Paint fgPaint;
  301. if (fgPaintStyle == null) {
  302. fgPaint = (Paint)firstLineAttr.getAttribute(TextAttribute.FOREGROUND);
  303. } else {
  304. fgPaint = new DrawPaint(ps).getPaint(graphics, fgPaintStyle);
  305. }
  306. float fontSize = (Float)firstLineAttr.getAttribute(TextAttribute.SIZE);
  307. Double buSz = bulletStyle.getBulletFontSize();
  308. if (buSz == null) {
  309. buSz = 100d;
  310. }
  311. if (buSz > 0) {
  312. fontSize *= buSz* 0.01;
  313. } else {
  314. fontSize = (float)-buSz;
  315. }
  316. String buFontStr = bulletStyle.getBulletFont();
  317. if (buFontStr == null) {
  318. buFontStr = paragraph.getDefaultFontFamily();
  319. }
  320. assert(buFontStr != null);
  321. FontInfo buFont = new DrawFontInfo(buFontStr);
  322. DrawFontManager dfm = DrawFactory.getInstance(graphics).getFontManager(graphics);
  323. // TODO: check font group defaulting to Symbol
  324. buFont = dfm.getMappedFont(graphics, buFont);
  325. Map<TextAttribute,Object> att = new HashMap<>();
  326. att.put(TextAttribute.FOREGROUND, fgPaint);
  327. att.put(TextAttribute.FAMILY, buFont.getTypeface());
  328. att.put(TextAttribute.SIZE, fontSize);
  329. att.put(TextAttribute.FONT, new Font(att));
  330. AttributedString str = new AttributedString(dfm.mapFontCharset(graphics,buFont,buCharacter));
  331. att.forEach(str::addAttribute);
  332. TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext());
  333. DrawFactory fact = DrawFactory.getInstance(graphics);
  334. return fact.getTextFragment(layout, str);
  335. }
  336. protected String getRenderableText(Graphics2D graphics, TextRun tr) {
  337. FieldType ft = tr.getFieldType();
  338. if (ft == null) {
  339. return getRenderableText(tr);
  340. }
  341. if (!tr.getRawText().isEmpty()) {
  342. switch (ft) {
  343. case SLIDE_NUMBER: {
  344. Slide<?, ?> slide = (Slide<?, ?>) graphics.getRenderingHint(Drawable.CURRENT_SLIDE);
  345. return (slide == null) ? "" : Integer.toString(slide.getSlideNumber());
  346. }
  347. case DATE_TIME: {
  348. PlaceholderDetails pd = ((SimpleShape<?, ?>) this.getParagraphShape()).getPlaceholderDetails();
  349. // refresh internal members
  350. pd.getPlaceholder();
  351. String uda = pd.getUserDate();
  352. if (uda != null) {
  353. return uda;
  354. }
  355. Calendar cal = LocaleUtil.getLocaleCalendar();
  356. LocalDateTime now = LocalDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
  357. return now.format(pd.getDateFormat());
  358. }
  359. }
  360. }
  361. return "";
  362. }
  363. @Internal
  364. public String getRenderableText(final TextRun tr) {
  365. String txtSpace = tr.getRawText();
  366. if (txtSpace == null) {
  367. return null;
  368. }
  369. if (txtSpace.contains("\t")) {
  370. txtSpace = txtSpace.replace("\t", tab2space(tr));
  371. }
  372. txtSpace = txtSpace.replace('\u000b', '\n');
  373. final Locale loc = LocaleUtil.getUserLocale();
  374. switch (tr.getTextCap()) {
  375. case ALL:
  376. return txtSpace.toUpperCase(loc);
  377. case SMALL:
  378. return txtSpace.toLowerCase(loc);
  379. default:
  380. return txtSpace;
  381. }
  382. }
  383. /**
  384. * Replace a tab with the effective number of white spaces.
  385. */
  386. private String tab2space(TextRun tr) {
  387. AttributedString string = new AttributedString(" ");
  388. String fontFamily = tr.getFontFamily();
  389. if (fontFamily == null) {
  390. fontFamily = "Lucida Sans";
  391. }
  392. string.addAttribute(TextAttribute.FAMILY, fontFamily);
  393. Double fs = tr.getFontSize();
  394. if (fs == null) {
  395. fs = 12d;
  396. }
  397. string.addAttribute(TextAttribute.SIZE, fs.floatValue());
  398. TextLayout l = new TextLayout(string.getIterator(), new FontRenderContext(null, true, true));
  399. // some JDK versions return 0 here
  400. double wspace = l.getAdvance();
  401. final int numSpaces;
  402. Double tabSz = paragraph.getDefaultTabSize();
  403. if (wspace <= 0) {
  404. numSpaces = 4;
  405. } else {
  406. if (tabSz == null) {
  407. tabSz = wspace*4;
  408. }
  409. numSpaces = (int)Math.min(Math.ceil(tabSz / wspace), 20);
  410. }
  411. char[] buf = new char[numSpaces];
  412. Arrays.fill(buf, ' ');
  413. return new String(buf);
  414. }
  415. /**
  416. * Returns wrapping width to break lines in this paragraph
  417. *
  418. * @param firstLine whether the first line is breaking
  419. *
  420. * @return wrapping width in points
  421. */
  422. protected double getWrappingWidth(boolean firstLine, Graphics2D graphics){
  423. final long TAB_SIZE = 347663L;
  424. TextShape<?,?> ts = paragraph.getParentShape();
  425. // internal margins for the text box
  426. Insets2D insets = ts.getInsets();
  427. double leftInset = insets.left;
  428. double rightInset = insets.right;
  429. int indentLevel = paragraph.getIndentLevel();
  430. if (indentLevel == -1) {
  431. // default to 0, if indentLevel is not set
  432. indentLevel = 0;
  433. }
  434. Double leftMargin = paragraph.getLeftMargin();
  435. if (leftMargin == null) {
  436. // if the marL attribute is omitted, then a value of 347663 is implied
  437. leftMargin = Units.toPoints(TAB_SIZE * indentLevel);
  438. }
  439. Double indent = paragraph.getIndent();
  440. if (indent == null) {
  441. indent = 0.;
  442. }
  443. Double rightMargin = paragraph.getRightMargin();
  444. if (rightMargin == null) {
  445. rightMargin = 0d;
  446. }
  447. Rectangle2D anchor = DrawShape.getAnchor(graphics, ts);
  448. TextDirection textDir = ts.getTextDirection();
  449. double width;
  450. if (!ts.getWordWrap()) {
  451. Dimension pageDim = ts.getSheet().getSlideShow().getPageSize();
  452. // if wordWrap == false then we return the advance to the (right) border of the sheet
  453. switch (textDir) {
  454. default:
  455. width = pageDim.getWidth() - anchor.getX();
  456. break;
  457. case VERTICAL:
  458. width = pageDim.getHeight() - anchor.getX();
  459. break;
  460. case VERTICAL_270:
  461. width = anchor.getX();
  462. break;
  463. }
  464. } else {
  465. switch (textDir) {
  466. default:
  467. width = anchor.getWidth() - leftInset - rightInset - leftMargin - rightMargin;
  468. break;
  469. case VERTICAL:
  470. case VERTICAL_270:
  471. width = anchor.getHeight() - leftInset - rightInset - leftMargin - rightMargin;
  472. break;
  473. }
  474. if (firstLine && bullet == null) {
  475. // indent is usually negative in XSLF
  476. width += isHSLF() ? (leftMargin - indent) : -indent;
  477. }
  478. }
  479. return width;
  480. }
  481. private static class AttributedStringData {
  482. Attribute attribute;
  483. Object value;
  484. int beginIndex, endIndex;
  485. AttributedStringData(Attribute attribute, Object value, int beginIndex, int endIndex) {
  486. this.attribute = attribute;
  487. this.value = value;
  488. this.beginIndex = beginIndex;
  489. this.endIndex = endIndex;
  490. }
  491. }
  492. /**
  493. * Helper method for paint style relative to bounds, e.g. gradient paint
  494. */
  495. private PlaceableShape<?,?> getParagraphShape() {
  496. return paragraph.getParentShape();
  497. }
  498. protected List<AttributedStringData> getAttributedString(Graphics2D graphics, StringBuilder text) {
  499. if (text == null) {
  500. text = new StringBuilder();
  501. }
  502. final DrawPaint dp = new DrawPaint(getParagraphShape());
  503. DrawFontManager dfm = DrawFactory.getInstance(graphics).getFontManager(graphics);
  504. assert(dfm != null);
  505. final Map<Attribute,Object> att = new HashMap<>();
  506. final List<AttributedStringData> attList = new ArrayList<>();
  507. for (TextRun run : paragraph){
  508. String runText = getRenderableText(graphics, run);
  509. // skip empty runs
  510. if (runText.isEmpty()) {
  511. continue;
  512. }
  513. att.clear();
  514. // user can pass an custom object to convert fonts
  515. FontInfo fontInfo = run.getFontInfo(null);
  516. runText = dfm.mapFontCharset(graphics, fontInfo, runText);
  517. final int beginIndex = text.length();
  518. text.append(runText);
  519. final int endIndex = text.length();
  520. PaintStyle fgPaintStyle = run.getFontColor();
  521. Paint fgPaint = dp.getPaint(graphics, fgPaintStyle);
  522. att.put(TextAttribute.FOREGROUND, fgPaint);
  523. Double fontSz = run.getFontSize();
  524. if (fontSz == null) {
  525. fontSz = paragraph.getDefaultFontSize();
  526. }
  527. att.put(TextAttribute.SIZE, fontSz.floatValue());
  528. if(run.isBold()) {
  529. att.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
  530. }
  531. if(run.isItalic()) {
  532. att.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
  533. }
  534. if(run.isUnderlined()) {
  535. att.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
  536. att.put(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL);
  537. }
  538. if(run.isStrikethrough()) {
  539. att.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
  540. }
  541. if(run.isSubscript()) {
  542. att.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB);
  543. }
  544. if(run.isSuperscript()) {
  545. att.put(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER);
  546. }
  547. Hyperlink<?,?> hl = run.getHyperlink();
  548. if (hl != null) {
  549. att.put(HYPERLINK_HREF, hl.getAddress());
  550. att.put(HYPERLINK_LABEL, hl.getLabel());
  551. }
  552. if (fontInfo != null) {
  553. att.put(TextAttribute.FAMILY, fontInfo.getTypeface());
  554. } else {
  555. att.put(TextAttribute.FAMILY, paragraph.getDefaultFontFamily());
  556. }
  557. att.put(TextAttribute.FONT, new Font(att));
  558. att.forEach((k,v) -> attList.add(new AttributedStringData(k,v,beginIndex,endIndex)));
  559. processGlyphs(graphics, dfm, attList, beginIndex, run, runText);
  560. }
  561. // ensure that the paragraph contains at least one character
  562. // We need this trick to correctly measure text
  563. if (text.length() == 0) {
  564. text.append(" ");
  565. Double fontSz = paragraph.getDefaultFontSize();
  566. att.put(TextAttribute.SIZE, fontSz.floatValue());
  567. att.put(TextAttribute.FAMILY, paragraph.getDefaultFontFamily());
  568. att.put(TextAttribute.FONT, new Font(att));
  569. att.forEach((k,v) -> attList.add(new AttributedStringData(k,v,0,1)));
  570. }
  571. return attList;
  572. }
  573. /**
  574. * Processing the glyphs is done in two steps:
  575. * <ul>
  576. * <li>1. determine the font group - a text run can have different font groups.
  577. * <li>2. Depending on the chars, the correct font group needs to be used
  578. * </ul>
  579. *
  580. * @see <a href="https://blogs.msdn.microsoft.com/officeinteroperability/2013/04/22/office-open-xml-themes-schemes-and-fonts/">Office Open XML Themes, Schemes, and Fonts</a>
  581. */
  582. private void processGlyphs(Graphics2D graphics, DrawFontManager dfm, List<AttributedStringData> attList, final int beginIndex, TextRun run, String runText) {
  583. // determine font group ranges of the textrun to focus the fallback handling only on that font group
  584. List<FontGroupRange> ttrList = FontGroup.getFontGroupRanges(runText);
  585. int rangeBegin = 0;
  586. for (FontGroupRange ttr : ttrList) {
  587. FontInfo fiRun = run.getFontInfo(ttr.getFontGroup());
  588. if (fiRun == null) {
  589. // if the font group specific font wasn't defined, fallback to LATIN
  590. fiRun = run.getFontInfo(FontGroup.LATIN);
  591. }
  592. FontInfo fiMapped = dfm.getMappedFont(graphics, fiRun);
  593. FontInfo fiFallback = dfm.getFallbackFont(graphics, fiRun);
  594. assert(fiFallback != null);
  595. if (fiMapped == null) {
  596. fiMapped = dfm.getMappedFont(graphics, new DrawFontInfo(paragraph.getDefaultFontFamily()));
  597. }
  598. if (fiMapped == null) {
  599. fiMapped = fiFallback;
  600. }
  601. Font fontMapped = dfm.createAWTFont(graphics, fiMapped, 10, run.isBold(), run.isItalic());
  602. Font fontFallback = dfm.createAWTFont(graphics, fiFallback, 10, run.isBold(), run.isItalic());
  603. // check for unsupported characters and add a fallback font for these
  604. final int rangeLen = ttr.getLength();
  605. int partEnd = rangeBegin;
  606. while (partEnd<rangeBegin+rangeLen) {
  607. // start with the assumption that the font is able to display the chars
  608. int partBegin = partEnd;
  609. partEnd = nextPart(fontMapped, runText, partBegin, rangeBegin+rangeLen, true);
  610. // Now we have 3 cases:
  611. // (a) the first part couldn't be displayed,
  612. // (b) only part of the text run could be displayed
  613. // (c) or all chars can be displayed (default)
  614. if (partBegin < partEnd) {
  615. // handle (b) and (c)
  616. final String fontName = fontMapped.getFontName(Locale.ROOT);
  617. final int startIndex = beginIndex + partBegin;
  618. final int endIndex = beginIndex + partEnd;
  619. attList.add(new AttributedStringData(TextAttribute.FAMILY, fontName, startIndex, endIndex));
  620. LOG.atDebug().log("mapped: {} {} {} - {}", fontName, box(startIndex),box(endIndex),runText.substring(partBegin, partEnd));
  621. }
  622. // fallback for unsupported glyphs
  623. partBegin = partEnd;
  624. partEnd = nextPart(fontMapped, runText, partBegin, rangeBegin+rangeLen, false);
  625. if (partBegin < partEnd) {
  626. // handle (a) and (b)
  627. final String fontName = fontFallback.getFontName(Locale.ROOT);
  628. final int startIndex = beginIndex + partBegin;
  629. final int endIndex = beginIndex + partEnd;
  630. attList.add(new AttributedStringData(TextAttribute.FAMILY, fontName, startIndex, endIndex));
  631. LOG.atDebug().log("fallback: {} {} {} - {}", fontName, box(startIndex),box(endIndex),runText.substring(partBegin, partEnd));
  632. }
  633. }
  634. rangeBegin += rangeLen;
  635. }
  636. }
  637. private static int nextPart(Font fontMapped, String runText, int beginPart, int endPart, boolean isDisplayed) {
  638. int rIdx = beginPart;
  639. while (rIdx < endPart) {
  640. int codepoint = runText.codePointAt(rIdx);
  641. if (fontMapped.canDisplay(codepoint) != isDisplayed) {
  642. break;
  643. }
  644. rIdx += Character.charCount(codepoint);
  645. }
  646. return rIdx;
  647. }
  648. /**
  649. * @return {@code true} if the HSLF implementation is used
  650. */
  651. protected boolean isHSLF() {
  652. return DrawShape.isHSLF(paragraph.getParentShape());
  653. }
  654. protected boolean isFirstParagraph() {
  655. return firstParagraph;
  656. }
  657. protected void setFirstParagraph(boolean firstParagraph) {
  658. this.firstParagraph = firstParagraph;
  659. }
  660. }