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.

AFPTextPainter.java 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /* $Id$ */
  18. package org.apache.fop.render.afp;
  19. import java.awt.Graphics2D;
  20. import java.awt.geom.Point2D;
  21. import java.awt.geom.Rectangle2D;
  22. import java.text.AttributedCharacterIterator;
  23. import java.text.CharacterIterator;
  24. import java.awt.font.TextAttribute;
  25. import java.awt.Shape;
  26. import java.awt.Paint;
  27. import java.awt.Color;
  28. import java.io.IOException;
  29. import java.util.List;
  30. import java.util.Iterator;
  31. import org.apache.commons.logging.Log;
  32. import org.apache.commons.logging.LogFactory;
  33. import org.apache.batik.dom.svg.SVGOMTextElement;
  34. import org.apache.batik.gvt.text.Mark;
  35. import org.apache.batik.gvt.TextPainter;
  36. import org.apache.batik.gvt.TextNode;
  37. import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
  38. import org.apache.batik.gvt.text.TextPaintInfo;
  39. import org.apache.batik.gvt.font.GVTFontFamily;
  40. import org.apache.batik.gvt.renderer.StrokingTextPainter;
  41. import org.apache.fop.fonts.Font;
  42. import org.apache.fop.fonts.FontInfo;
  43. import org.apache.fop.fonts.FontTriplet;
  44. /**
  45. * Renders the attributed character iterator of a <tt>TextNode</tt>.
  46. * This class draws the text directly into the AFPGraphics2D so that
  47. * the text is not drawn using shapes.
  48. * If the text is simple enough to draw then it sets the font and calls
  49. * drawString. If the text is complex or the cannot be translated
  50. * into a simple drawString the StrokingTextPainter is used instead.
  51. */
  52. public class AFPTextPainter implements TextPainter {
  53. /** the logger for this class */
  54. protected Log log = LogFactory.getLog(AFPTextPainter.class);
  55. private AFPTextHandler nativeTextHandler;
  56. //private FontInfo fontInfo;
  57. /**
  58. * Use the stroking text painter to get the bounds and shape.
  59. * Also used as a fallback to draw the string with strokes.
  60. */
  61. protected static final TextPainter
  62. PROXY_PAINTER = StrokingTextPainter.getInstance();
  63. /**
  64. * Create a new PS text painter with the given font information.
  65. * @param nativeTextHandler the NativeTextHandler instance used for text painting
  66. */
  67. public AFPTextPainter(AFPTextHandler nativeTextHandler) {
  68. this.nativeTextHandler = nativeTextHandler;
  69. }
  70. /**
  71. * Paints the specified attributed character iterator using the
  72. * specified Graphics2D and context and font context.
  73. * @param node the TextNode to paint
  74. * @param g2d the Graphics2D to use
  75. */
  76. public void paint(TextNode node, Graphics2D g2d) {
  77. Point2D loc = node.getLocation();
  78. log.debug("painting text node " + node);
  79. if (hasUnsupportedAttributes(node)) {
  80. log.debug("hasunsuportedattributes");
  81. PROXY_PAINTER.paint(node, g2d);
  82. } else {
  83. log.debug("allattributessupported");
  84. paintTextRuns(node.getTextRuns(), g2d, loc);
  85. }
  86. }
  87. private boolean hasUnsupportedAttributes(TextNode node) {
  88. Iterator iter = node.getTextRuns().iterator();
  89. while (iter.hasNext()) {
  90. StrokingTextPainter.TextRun
  91. run = (StrokingTextPainter.TextRun)iter.next();
  92. AttributedCharacterIterator aci = run.getACI();
  93. boolean hasUnsupported = hasUnsupportedAttributes(aci);
  94. if (hasUnsupported) {
  95. return true;
  96. }
  97. }
  98. return false;
  99. }
  100. private boolean hasUnsupportedAttributes(AttributedCharacterIterator aci) {
  101. boolean hasunsupported = false;
  102. String text = getText(aci);
  103. Font font = makeFont(aci);
  104. if (hasUnsupportedGlyphs(text, font)) {
  105. log.trace("-> Unsupported glyphs found");
  106. hasunsupported = true;
  107. }
  108. TextPaintInfo tpi = (TextPaintInfo) aci.getAttribute(
  109. GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO);
  110. if ((tpi != null)
  111. && ((tpi.strokeStroke != null && tpi.strokePaint != null)
  112. || (tpi.strikethroughStroke != null)
  113. || (tpi.underlineStroke != null)
  114. || (tpi.overlineStroke != null))) {
  115. log.trace("-> under/overlines etc. found");
  116. hasunsupported = true;
  117. }
  118. //Alpha is not supported
  119. Paint foreground = (Paint) aci.getAttribute(TextAttribute.FOREGROUND);
  120. if (foreground instanceof Color) {
  121. Color col = (Color)foreground;
  122. if (col.getAlpha() != 255) {
  123. log.trace("-> transparency found");
  124. hasunsupported = true;
  125. }
  126. }
  127. Object letSpace = aci.getAttribute(
  128. GVTAttributedCharacterIterator.TextAttribute.LETTER_SPACING);
  129. if (letSpace != null) {
  130. log.trace("-> letter spacing found");
  131. hasunsupported = true;
  132. }
  133. Object wordSpace = aci.getAttribute(
  134. GVTAttributedCharacterIterator.TextAttribute.WORD_SPACING);
  135. if (wordSpace != null) {
  136. log.trace("-> word spacing found");
  137. hasunsupported = true;
  138. }
  139. Object lengthAdjust = aci.getAttribute(
  140. GVTAttributedCharacterIterator.TextAttribute.LENGTH_ADJUST);
  141. if (lengthAdjust != null) {
  142. log.trace("-> length adjustments found");
  143. hasunsupported = true;
  144. }
  145. Object writeMod = aci.getAttribute(
  146. GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE);
  147. if (writeMod != null
  148. && !GVTAttributedCharacterIterator.TextAttribute.WRITING_MODE_LTR.equals(
  149. writeMod)) {
  150. log.trace("-> Unsupported writing modes found");
  151. hasunsupported = true;
  152. }
  153. Object vertOr = aci.getAttribute(
  154. GVTAttributedCharacterIterator.TextAttribute.VERTICAL_ORIENTATION);
  155. if (GVTAttributedCharacterIterator.TextAttribute.ORIENTATION_ANGLE.equals(
  156. vertOr)) {
  157. log.trace("-> vertical orientation found");
  158. hasunsupported = true;
  159. }
  160. Object rcDel = aci.getAttribute(
  161. GVTAttributedCharacterIterator.TextAttribute.TEXT_COMPOUND_DELIMITER);
  162. //Batik 1.6 returns null here which makes it impossible to determine whether this can
  163. //be painted or not, i.e. fall back to stroking. :-(
  164. if (rcDel != null && !(rcDel instanceof SVGOMTextElement)) {
  165. log.trace("-> spans found");
  166. hasunsupported = true; //Filter spans
  167. }
  168. if (hasunsupported) {
  169. log.trace("Unsupported attributes found in ACI, using StrokingTextPainter");
  170. }
  171. return hasunsupported;
  172. }
  173. /**
  174. * Paint a list of text runs on the Graphics2D at a given location.
  175. * @param textRuns the list of text runs
  176. * @param g2d the Graphics2D to paint to
  177. * @param loc the current location of the "cursor"
  178. */
  179. protected void paintTextRuns(List textRuns, Graphics2D g2d, Point2D loc) {
  180. Point2D currentloc = loc;
  181. Iterator i = textRuns.iterator();
  182. while (i.hasNext()) {
  183. StrokingTextPainter.TextRun
  184. run = (StrokingTextPainter.TextRun)i.next();
  185. currentloc = paintTextRun(run, g2d, currentloc);
  186. }
  187. }
  188. /**
  189. * Paint a single text run on the Graphics2D at a given location.
  190. * @param run the text run to paint
  191. * @param g2d the Graphics2D to paint to
  192. * @param loc the current location of the "cursor"
  193. * @return the new location of the "cursor" after painting the text run
  194. */
  195. protected Point2D paintTextRun(StrokingTextPainter.TextRun run, Graphics2D g2d, Point2D loc) {
  196. AttributedCharacterIterator aci = run.getACI();
  197. aci.first();
  198. updateLocationFromACI(aci, loc);
  199. loc = g2d.getTransform().transform(loc, null);
  200. // font
  201. Font font = makeFont(aci);
  202. nativeTextHandler.setOverrideFont(font);
  203. // color
  204. TextPaintInfo tpi = (TextPaintInfo) aci.getAttribute(
  205. GVTAttributedCharacterIterator.TextAttribute.PAINT_INFO);
  206. if (tpi == null) {
  207. return loc;
  208. }
  209. Paint foreground = tpi.fillPaint;
  210. if (foreground instanceof Color) {
  211. Color col = (Color)foreground;
  212. g2d.setColor(col);
  213. }
  214. g2d.setPaint(foreground);
  215. String txt = getText(aci);
  216. float advance = getStringWidth(txt, font);
  217. float tx = 0;
  218. TextNode.Anchor anchor = (TextNode.Anchor)aci.getAttribute(
  219. GVTAttributedCharacterIterator.TextAttribute.ANCHOR_TYPE);
  220. if (anchor != null) {
  221. switch (anchor.getType()) {
  222. case TextNode.Anchor.ANCHOR_MIDDLE:
  223. tx = -advance / 2;
  224. break;
  225. case TextNode.Anchor.ANCHOR_END:
  226. tx = -advance;
  227. break;
  228. default: //nop
  229. }
  230. }
  231. // draw string
  232. try {
  233. try {
  234. nativeTextHandler.drawString(txt, (float)(loc.getX() + tx), (float)(loc.getY()));
  235. } catch (IOException ioe) {
  236. if (g2d instanceof AFPGraphics2D) {
  237. ((AFPGraphics2D)g2d).handleIOException(ioe);
  238. }
  239. }
  240. } finally {
  241. nativeTextHandler.setOverrideFont(null);
  242. }
  243. loc.setLocation(loc.getX() + (double)advance, loc.getY());
  244. return loc;
  245. }
  246. /**
  247. * Extract the raw text from an ACI.
  248. * @param aci ACI to inspect
  249. * @return the extracted text
  250. */
  251. protected String getText(AttributedCharacterIterator aci) {
  252. StringBuffer sb = new StringBuffer(aci.getEndIndex() - aci.getBeginIndex());
  253. for (char c = aci.first(); c != CharacterIterator.DONE; c = aci.next()) {
  254. sb.append(c);
  255. }
  256. return sb.toString();
  257. }
  258. private void updateLocationFromACI(
  259. AttributedCharacterIterator aci,
  260. Point2D loc) {
  261. //Adjust position of span
  262. Float xpos = (Float)aci.getAttribute(
  263. GVTAttributedCharacterIterator.TextAttribute.X);
  264. Float ypos = (Float)aci.getAttribute(
  265. GVTAttributedCharacterIterator.TextAttribute.Y);
  266. Float dxpos = (Float)aci.getAttribute(
  267. GVTAttributedCharacterIterator.TextAttribute.DX);
  268. Float dypos = (Float)aci.getAttribute(
  269. GVTAttributedCharacterIterator.TextAttribute.DY);
  270. if (xpos != null) {
  271. loc.setLocation(xpos.doubleValue(), loc.getY());
  272. }
  273. if (ypos != null) {
  274. loc.setLocation(loc.getX(), ypos.doubleValue());
  275. }
  276. if (dxpos != null) {
  277. loc.setLocation(loc.getX() + dxpos.doubleValue(), loc.getY());
  278. }
  279. if (dypos != null) {
  280. loc.setLocation(loc.getX(), loc.getY() + dypos.doubleValue());
  281. }
  282. }
  283. private String getStyle(AttributedCharacterIterator aci) {
  284. Float posture = (Float)aci.getAttribute(TextAttribute.POSTURE);
  285. return ((posture != null) && (posture.floatValue() > 0.0))
  286. ? "italic"
  287. : "normal";
  288. }
  289. private int getWeight(AttributedCharacterIterator aci) {
  290. Float taWeight = (Float)aci.getAttribute(TextAttribute.WEIGHT);
  291. return ((taWeight != null) && (taWeight.floatValue() > 1.0))
  292. ? Font.WEIGHT_BOLD
  293. : Font.WEIGHT_NORMAL;
  294. }
  295. private Font makeFont(AttributedCharacterIterator aci) {
  296. Float fontSize = (Float)aci.getAttribute(TextAttribute.SIZE);
  297. if (fontSize == null) {
  298. fontSize = new Float(10.0f);
  299. }
  300. String style = getStyle(aci);
  301. int weight = getWeight(aci);
  302. FontInfo fontInfo = nativeTextHandler.getFontInfo();
  303. String fontFamily = null;
  304. List gvtFonts = (List) aci.getAttribute(
  305. GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES);
  306. if (gvtFonts != null) {
  307. Iterator i = gvtFonts.iterator();
  308. while (i.hasNext()) {
  309. GVTFontFamily fam = (GVTFontFamily) i.next();
  310. /* (todo) Enable SVG Font painting
  311. if (fam instanceof SVGFontFamily) {
  312. PROXY_PAINTER.paint(node, g2d);
  313. return;
  314. }*/
  315. fontFamily = fam.getFamilyName();
  316. if (fontInfo.hasFont(fontFamily, style, weight)) {
  317. FontTriplet triplet = fontInfo.fontLookup(
  318. fontFamily, style, weight);
  319. int fsize = (int)(fontSize.floatValue() * 1000);
  320. return fontInfo.getFontInstance(triplet, fsize);
  321. }
  322. }
  323. }
  324. FontTriplet triplet = fontInfo.fontLookup("any", style, Font.WEIGHT_NORMAL);
  325. int fsize = (int)(fontSize.floatValue() * 1000);
  326. return fontInfo.getFontInstance(triplet, fsize);
  327. }
  328. private float getStringWidth(String str, Font font) {
  329. float wordWidth = 0;
  330. float whitespaceWidth = font.getWidth(font.mapChar(' '));
  331. for (int i = 0; i < str.length(); i++) {
  332. float charWidth;
  333. char c = str.charAt(i);
  334. if (!((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t'))) {
  335. charWidth = font.getWidth(font.mapChar(c));
  336. if (charWidth <= 0) {
  337. charWidth = whitespaceWidth;
  338. }
  339. } else {
  340. charWidth = whitespaceWidth;
  341. }
  342. wordWidth += charWidth;
  343. }
  344. return wordWidth / 1000f;
  345. }
  346. private boolean hasUnsupportedGlyphs(String str, Font font) {
  347. for (int i = 0; i < str.length(); i++) {
  348. char c = str.charAt(i);
  349. if (!((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t'))) {
  350. if (!font.hasChar(c)) {
  351. return true;
  352. }
  353. }
  354. }
  355. return false;
  356. }
  357. /**
  358. * Get the outline shape of the text characters.
  359. * This uses the StrokingTextPainter to get the outline
  360. * shape since in theory it should be the same.
  361. *
  362. * @param node the text node
  363. * @return the outline shape of the text characters
  364. */
  365. public Shape getOutline(TextNode node) {
  366. return PROXY_PAINTER.getOutline(node);
  367. }
  368. /**
  369. * Get the bounds.
  370. * This uses the StrokingTextPainter to get the bounds
  371. * since in theory it should be the same.
  372. *
  373. * @param node the text node
  374. * @return the bounds of the text
  375. */
  376. public Rectangle2D getBounds2D(TextNode node) {
  377. /* (todo) getBounds2D() is too slow
  378. * because it uses the StrokingTextPainter. We should implement this
  379. * method ourselves. */
  380. return PROXY_PAINTER.getBounds2D(node);
  381. }
  382. /**
  383. * Get the geometry bounds.
  384. * This uses the StrokingTextPainter to get the bounds
  385. * since in theory it should be the same.
  386. * @param node the text node
  387. * @return the bounds of the text
  388. */
  389. public Rectangle2D getGeometryBounds(TextNode node) {
  390. return PROXY_PAINTER.getGeometryBounds(node);
  391. }
  392. // Methods that have no purpose for PS
  393. /**
  394. * Get the mark.
  395. * This does nothing since the output is pdf and not interactive.
  396. * @param node the text node
  397. * @param pos the position
  398. * @param all select all
  399. * @return null
  400. */
  401. public Mark getMark(TextNode node, int pos, boolean all) {
  402. return null;
  403. }
  404. /**
  405. * Select at.
  406. * This does nothing since the output is pdf and not interactive.
  407. * @param x the x position
  408. * @param y the y position
  409. * @param node the text node
  410. * @return null
  411. */
  412. public Mark selectAt(double x, double y, TextNode node) {
  413. return null;
  414. }
  415. /**
  416. * Select to.
  417. * This does nothing since the output is pdf and not interactive.
  418. * @param x the x position
  419. * @param y the y position
  420. * @param beginMark the start mark
  421. * @return null
  422. */
  423. public Mark selectTo(double x, double y, Mark beginMark) {
  424. return null;
  425. }
  426. /**
  427. * Selec first.
  428. * This does nothing since the output is pdf and not interactive.
  429. * @param node the text node
  430. * @return null
  431. */
  432. public Mark selectFirst(TextNode node) {
  433. return null;
  434. }
  435. /**
  436. * Select last.
  437. * This does nothing since the output is pdf and not interactive.
  438. * @param node the text node
  439. * @return null
  440. */
  441. public Mark selectLast(TextNode node) {
  442. return null;
  443. }
  444. /**
  445. * Get selected.
  446. * This does nothing since the output is pdf and not interactive.
  447. * @param start the start mark
  448. * @param finish the finish mark
  449. * @return null
  450. */
  451. public int[] getSelected(Mark start, Mark finish) {
  452. return null;
  453. }
  454. /**
  455. * Get the highlighted shape.
  456. * This does nothing since the output is pdf and not interactive.
  457. * @param beginMark the start mark
  458. * @param endMark the end mark
  459. * @return null
  460. */
  461. public Shape getHighlightShape(Mark beginMark, Mark endMark) {
  462. return null;
  463. }
  464. }