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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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.ps;
  19. import java.awt.Color;
  20. import java.awt.Graphics2D;
  21. import java.awt.Paint;
  22. import java.awt.Shape;
  23. import java.awt.Stroke;
  24. import java.awt.font.TextAttribute;
  25. import java.awt.geom.Point2D;
  26. import java.awt.geom.Rectangle2D;
  27. import java.io.IOException;
  28. import java.text.AttributedCharacterIterator;
  29. import java.text.CharacterIterator;
  30. import java.util.Iterator;
  31. import java.util.List;
  32. import org.apache.batik.dom.svg.SVGOMTextElement;
  33. import org.apache.batik.gvt.TextNode;
  34. import org.apache.batik.gvt.TextPainter;
  35. import org.apache.batik.gvt.font.GVTFontFamily;
  36. import org.apache.batik.gvt.renderer.StrokingTextPainter;
  37. import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;
  38. import org.apache.batik.gvt.text.Mark;
  39. import org.apache.batik.gvt.text.TextPaintInfo;
  40. import org.apache.commons.logging.Log;
  41. import org.apache.commons.logging.LogFactory;
  42. import org.apache.fop.fonts.Font;
  43. import org.apache.fop.fonts.FontInfo;
  44. import org.apache.fop.fonts.FontTriplet;
  45. import org.apache.xmlgraphics.java2d.ps.PSGraphics2D;
  46. /**
  47. * Renders the attributed character iterator of a <tt>TextNode</tt>.
  48. * This class draws the text directly into the PSGraphics2D so that
  49. * the text is not drawn using shapes which makes the PS files larger.
  50. * If the text is simple enough to draw then it sets the font and calls
  51. * drawString. If the text is complex or the cannot be translated
  52. * into a simple drawString the StrokingTextPainter is used instead.
  53. *
  54. * (todo) handle underline, overline and strikethrough
  55. * (todo) use drawString(AttributedCharacterIterator iterator...) for some
  56. *
  57. * @author <a href="mailto:keiron@aftexsw.com">Keiron Liddle</a>
  58. * @version $Id$
  59. */
  60. public class PSTextPainter implements TextPainter {
  61. /** the logger for this class */
  62. protected Log log = LogFactory.getLog(PSTextPainter.class);
  63. private final NativeTextHandler nativeTextHandler;
  64. private final FontInfo fontInfo;
  65. /**
  66. * Use the stroking text painter to get the bounds and shape.
  67. * Also used as a fallback to draw the string with strokes.
  68. */
  69. protected static final TextPainter
  70. PROXY_PAINTER = StrokingTextPainter.getInstance();
  71. /**
  72. * Create a new PS text painter with the given font information.
  73. * @param nativeTextHandler the NativeTextHandler instance used for text painting
  74. */
  75. public PSTextPainter(NativeTextHandler nativeTextHandler) {
  76. this.nativeTextHandler = nativeTextHandler;
  77. this.fontInfo = nativeTextHandler.getFontInfo();
  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. drawPrimitiveString(g2d, loc, font, txt, tx);
  273. loc.setLocation(loc.getX() + advance, loc.getY());
  274. return loc;
  275. }
  276. protected void drawPrimitiveString(Graphics2D g2d, Point2D loc, Font font, String txt, float tx) {
  277. //Finally draw text
  278. nativeTextHandler.setOverrideFont(font);
  279. try {
  280. try {
  281. nativeTextHandler.drawString(txt, (float)(loc.getX() + tx), (float)(loc.getY()));
  282. } catch (IOException ioe) {
  283. if (g2d instanceof PSGraphics2D) {
  284. ((PSGraphics2D)g2d).handleIOException(ioe);
  285. }
  286. }
  287. } finally {
  288. nativeTextHandler.setOverrideFont(null);
  289. }
  290. }
  291. private void updateLocationFromACI(
  292. AttributedCharacterIterator aci,
  293. Point2D loc) {
  294. //Adjust position of span
  295. Float xpos = (Float)aci.getAttribute(
  296. GVTAttributedCharacterIterator.TextAttribute.X);
  297. Float ypos = (Float)aci.getAttribute(
  298. GVTAttributedCharacterIterator.TextAttribute.Y);
  299. Float dxpos = (Float)aci.getAttribute(
  300. GVTAttributedCharacterIterator.TextAttribute.DX);
  301. Float dypos = (Float)aci.getAttribute(
  302. GVTAttributedCharacterIterator.TextAttribute.DY);
  303. if (xpos != null) {
  304. loc.setLocation(xpos.doubleValue(), loc.getY());
  305. }
  306. if (ypos != null) {
  307. loc.setLocation(loc.getX(), ypos.doubleValue());
  308. }
  309. if (dxpos != null) {
  310. loc.setLocation(loc.getX() + dxpos.doubleValue(), loc.getY());
  311. }
  312. if (dypos != null) {
  313. loc.setLocation(loc.getX(), loc.getY() + dypos.doubleValue());
  314. }
  315. }
  316. private String getStyle(AttributedCharacterIterator aci) {
  317. Float posture = (Float)aci.getAttribute(TextAttribute.POSTURE);
  318. return ((posture != null) && (posture.floatValue() > 0.0))
  319. ? "italic"
  320. : "normal";
  321. }
  322. private int getWeight(AttributedCharacterIterator aci) {
  323. Float taWeight = (Float)aci.getAttribute(TextAttribute.WEIGHT);
  324. return ((taWeight != null) && (taWeight.floatValue() > 1.0))
  325. ? Font.WEIGHT_BOLD
  326. : Font.WEIGHT_NORMAL;
  327. }
  328. private Font makeFont(AttributedCharacterIterator aci) {
  329. Float fontSize = (Float)aci.getAttribute(TextAttribute.SIZE);
  330. if (fontSize == null) {
  331. fontSize = new Float(10.0f);
  332. }
  333. String style = getStyle(aci);
  334. int weight = getWeight(aci);
  335. String fontFamily = null;
  336. List gvtFonts = (List) aci.getAttribute(
  337. GVTAttributedCharacterIterator.TextAttribute.GVT_FONT_FAMILIES);
  338. if (gvtFonts != null) {
  339. Iterator i = gvtFonts.iterator();
  340. while (i.hasNext()) {
  341. GVTFontFamily fam = (GVTFontFamily) i.next();
  342. /* (todo) Enable SVG Font painting
  343. if (fam instanceof SVGFontFamily) {
  344. PROXY_PAINTER.paint(node, g2d);
  345. return;
  346. }*/
  347. fontFamily = fam.getFamilyName();
  348. if (fontInfo.hasFont(fontFamily, style, weight)) {
  349. FontTriplet triplet = fontInfo.fontLookup(
  350. fontFamily, style, weight);
  351. int fsize = (int)(fontSize.floatValue() * 1000);
  352. return fontInfo.getFontInstance(triplet, fsize);
  353. }
  354. }
  355. }
  356. FontTriplet triplet = fontInfo.fontLookup("any", style, Font.WEIGHT_NORMAL);
  357. int fsize = (int)(fontSize.floatValue() * 1000);
  358. return fontInfo.getFontInstance(triplet, fsize);
  359. }
  360. private java.awt.Font makeAWTFont(AttributedCharacterIterator aci, Font font) {
  361. final String style = getStyle(aci);
  362. final int weight = getWeight(aci);
  363. int fStyle = java.awt.Font.PLAIN;
  364. if (weight == Font.WEIGHT_BOLD) {
  365. fStyle |= java.awt.Font.BOLD;
  366. }
  367. if ("italic".equals(style)) {
  368. fStyle |= java.awt.Font.ITALIC;
  369. }
  370. return new java.awt.Font(font.getFontName(), fStyle,
  371. (font.getFontSize() / 1000));
  372. }
  373. private float getStringWidth(String str, Font font) {
  374. float wordWidth = 0;
  375. float whitespaceWidth = font.getWidth(font.mapChar(' '));
  376. for (int i = 0; i < str.length(); i++) {
  377. float charWidth;
  378. char c = str.charAt(i);
  379. if (!((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t'))) {
  380. charWidth = font.getWidth(font.mapChar(c));
  381. if (charWidth <= 0) {
  382. charWidth = whitespaceWidth;
  383. }
  384. } else {
  385. charWidth = whitespaceWidth;
  386. }
  387. wordWidth += charWidth;
  388. }
  389. return wordWidth / 1000f;
  390. }
  391. private boolean hasUnsupportedGlyphs(String str, Font font) {
  392. for (int i = 0; i < str.length(); i++) {
  393. float charWidth;
  394. char c = str.charAt(i);
  395. if (!((c == ' ') || (c == '\n') || (c == '\r') || (c == '\t'))) {
  396. if (!font.hasChar(c)) {
  397. return true;
  398. }
  399. }
  400. }
  401. return false;
  402. }
  403. /**
  404. * Get the outline shape of the text characters.
  405. * This uses the StrokingTextPainter to get the outline
  406. * shape since in theory it should be the same.
  407. *
  408. * @param node the text node
  409. * @return the outline shape of the text characters
  410. */
  411. public Shape getOutline(TextNode node) {
  412. return PROXY_PAINTER.getOutline(node);
  413. }
  414. /**
  415. * Get the bounds.
  416. * This uses the StrokingTextPainter to get the bounds
  417. * since in theory it should be the same.
  418. *
  419. * @param node the text node
  420. * @return the bounds of the text
  421. */
  422. public Rectangle2D getBounds2D(TextNode node) {
  423. /* (todo) getBounds2D() is too slow
  424. * because it uses the StrokingTextPainter. We should implement this
  425. * method ourselves. */
  426. return PROXY_PAINTER.getBounds2D(node);
  427. }
  428. /**
  429. * Get the geometry bounds.
  430. * This uses the StrokingTextPainter to get the bounds
  431. * since in theory it should be the same.
  432. * @param node the text node
  433. * @return the bounds of the text
  434. */
  435. public Rectangle2D getGeometryBounds(TextNode node) {
  436. return PROXY_PAINTER.getGeometryBounds(node);
  437. }
  438. // Methods that have no purpose for PS
  439. /**
  440. * Get the mark.
  441. * This does nothing since the output is pdf and not interactive.
  442. * @param node the text node
  443. * @param pos the position
  444. * @param all select all
  445. * @return null
  446. */
  447. public Mark getMark(TextNode node, int pos, boolean all) {
  448. return null;
  449. }
  450. /**
  451. * Select at.
  452. * This does nothing since the output is pdf and not interactive.
  453. * @param x the x position
  454. * @param y the y position
  455. * @param node the text node
  456. * @return null
  457. */
  458. public Mark selectAt(double x, double y, TextNode node) {
  459. return null;
  460. }
  461. /**
  462. * Select to.
  463. * This does nothing since the output is pdf and not interactive.
  464. * @param x the x position
  465. * @param y the y position
  466. * @param beginMark the start mark
  467. * @return null
  468. */
  469. public Mark selectTo(double x, double y, Mark beginMark) {
  470. return null;
  471. }
  472. /**
  473. * Selec first.
  474. * This does nothing since the output is pdf and not interactive.
  475. * @param node the text node
  476. * @return null
  477. */
  478. public Mark selectFirst(TextNode node) {
  479. return null;
  480. }
  481. /**
  482. * Select last.
  483. * This does nothing since the output is pdf and not interactive.
  484. * @param node the text node
  485. * @return null
  486. */
  487. public Mark selectLast(TextNode node) {
  488. return null;
  489. }
  490. /**
  491. * Get selected.
  492. * This does nothing since the output is pdf and not interactive.
  493. * @param start the start mark
  494. * @param finish the finish mark
  495. * @return null
  496. */
  497. public int[] getSelected(Mark start, Mark finish) {
  498. return null;
  499. }
  500. /**
  501. * Get the highlighted shape.
  502. * This does nothing since the output is pdf and not interactive.
  503. * @param beginMark the start mark
  504. * @param endMark the end mark
  505. * @return null
  506. */
  507. public Shape getHighlightShape(Mark beginMark, Mark endMark) {
  508. return null;
  509. }
  510. }