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.

PSTextPainter.java 19KB

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