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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558
  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.BasicStroke;
  20. import java.awt.Color;
  21. import java.awt.Graphics2D;
  22. import java.awt.Paint;
  23. import java.awt.Shape;
  24. import java.awt.Stroke;
  25. import java.awt.geom.AffineTransform;
  26. import java.awt.geom.Ellipse2D;
  27. import java.awt.geom.GeneralPath;
  28. import java.awt.geom.PathIterator;
  29. import java.awt.geom.Point2D;
  30. import java.io.IOException;
  31. import java.text.AttributedCharacterIterator;
  32. import java.util.Iterator;
  33. import java.util.List;
  34. import org.apache.batik.gvt.font.GVTGlyphVector;
  35. import org.apache.batik.gvt.text.TextPaintInfo;
  36. import org.apache.batik.gvt.text.TextSpanLayout;
  37. import org.apache.xmlgraphics.java2d.ps.PSGraphics2D;
  38. import org.apache.xmlgraphics.ps.PSGenerator;
  39. import org.apache.fop.fonts.Font;
  40. import org.apache.fop.fonts.FontInfo;
  41. import org.apache.fop.fonts.FontMetrics;
  42. import org.apache.fop.fonts.LazyFont;
  43. import org.apache.fop.fonts.MultiByteFont;
  44. import org.apache.fop.svg.NativeTextPainter;
  45. import org.apache.fop.util.CharUtilities;
  46. import org.apache.fop.util.HexEncoder;
  47. /**
  48. * Renders the attributed character iterator of a text node.
  49. * This class draws the text directly using PostScript text operators so
  50. * the text is not drawn using shapes which makes the PS files larger.
  51. * <p>
  52. * The text runs are split into smaller text runs that can be bundles in single
  53. * calls of the xshow, yshow or xyshow operators. For outline text, the charpath
  54. * operator is used.
  55. */
  56. public class PSTextPainter extends NativeTextPainter {
  57. private static final boolean DEBUG = false;
  58. private FontResourceCache fontResources;
  59. private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform();
  60. /**
  61. * Create a new PS text painter with the given font information.
  62. * @param fontInfo the font collection
  63. */
  64. public PSTextPainter(FontInfo fontInfo) {
  65. super(fontInfo);
  66. this.fontResources = new FontResourceCache(fontInfo);
  67. }
  68. /** {@inheritDoc} */
  69. protected boolean isSupported(Graphics2D g2d) {
  70. return g2d instanceof PSGraphics2D;
  71. }
  72. /** {@inheritDoc} */
  73. protected void paintTextRun(TextRun textRun, Graphics2D g2d) throws IOException {
  74. AttributedCharacterIterator runaci = textRun.getACI();
  75. runaci.first();
  76. TextPaintInfo tpi = (TextPaintInfo)runaci.getAttribute(PAINT_INFO);
  77. if (tpi == null || !tpi.visible) {
  78. return;
  79. }
  80. if ((tpi != null) && (tpi.composite != null)) {
  81. g2d.setComposite(tpi.composite);
  82. }
  83. //------------------------------------
  84. TextSpanLayout layout = textRun.getLayout();
  85. logTextRun(runaci, layout);
  86. CharSequence chars = collectCharacters(runaci);
  87. runaci.first(); //Reset ACI
  88. final PSGraphics2D ps = (PSGraphics2D)g2d;
  89. final PSGenerator gen = ps.getPSGenerator();
  90. ps.preparePainting();
  91. if (DEBUG) {
  92. log.debug("Text: " + chars);
  93. gen.commentln("%Text: " + chars);
  94. }
  95. GeneralPath debugShapes = null;
  96. if (DEBUG) {
  97. debugShapes = new GeneralPath();
  98. }
  99. TextUtil textUtil = new TextUtil(gen);
  100. textUtil.setupFonts(runaci);
  101. if (!textUtil.hasFonts()) {
  102. //Draw using Java2D when no native fonts are available
  103. textRun.getLayout().draw(g2d);
  104. return;
  105. }
  106. gen.saveGraphicsState();
  107. gen.concatMatrix(g2d.getTransform());
  108. Shape imclip = g2d.getClip();
  109. clip(ps, imclip);
  110. gen.writeln("BT"); //beginTextObject()
  111. AffineTransform localTransform = new AffineTransform();
  112. Point2D prevPos = null;
  113. GVTGlyphVector gv = layout.getGlyphVector();
  114. PSTextRun psRun = new PSTextRun(); //Used to split a text run into smaller runs
  115. for (int index = 0, c = gv.getNumGlyphs(); index < c; index++) {
  116. char ch = chars.charAt(index);
  117. boolean visibleChar = gv.isGlyphVisible(index)
  118. || (CharUtilities.isAnySpace(ch) && !CharUtilities.isZeroWidthSpace(ch));
  119. logCharacter(ch, layout, index, visibleChar);
  120. if (!visibleChar) {
  121. continue;
  122. }
  123. Point2D glyphPos = gv.getGlyphPosition(index);
  124. AffineTransform glyphTransform = gv.getGlyphTransform(index);
  125. if (log.isTraceEnabled()) {
  126. log.trace("pos " + glyphPos + ", transform " + glyphTransform);
  127. }
  128. if (DEBUG) {
  129. Shape sh = gv.getGlyphLogicalBounds(index);
  130. if (sh == null) {
  131. sh = new Ellipse2D.Double(glyphPos.getX(), glyphPos.getY(), 2, 2);
  132. }
  133. debugShapes.append(sh, false);
  134. }
  135. //Exact position of the glyph
  136. localTransform.setToIdentity();
  137. localTransform.translate(glyphPos.getX(), glyphPos.getY());
  138. if (glyphTransform != null) {
  139. localTransform.concatenate(glyphTransform);
  140. }
  141. localTransform.scale(1, -1);
  142. boolean flushCurrentRun = false;
  143. //Try to optimize by combining characters using the same font and on the same line.
  144. if (glyphTransform != null) {
  145. //Happens for text-on-a-path
  146. flushCurrentRun = true;
  147. }
  148. if (psRun.getRunLength() >= 128) {
  149. //Don't let a run get too long
  150. flushCurrentRun = true;
  151. }
  152. //Note the position of the glyph relative to the previous one
  153. Point2D relPos;
  154. if (prevPos == null) {
  155. relPos = new Point2D.Double(0, 0);
  156. } else {
  157. relPos = new Point2D.Double(
  158. glyphPos.getX() - prevPos.getX(),
  159. glyphPos.getY() - prevPos.getY());
  160. }
  161. if (psRun.vertChanges == 0
  162. && psRun.getHorizRunLength() > 2
  163. && relPos.getY() != 0) {
  164. //new line
  165. flushCurrentRun = true;
  166. }
  167. //Select the actual character to paint
  168. char paintChar = (CharUtilities.isAnySpace(ch) ? ' ' : ch);
  169. //Select (sub)font for character
  170. Font f = textUtil.selectFontForChar(paintChar);
  171. char mapped = f.mapChar(ch);
  172. boolean fontChanging = textUtil.isFontChanging(f, mapped);
  173. if (fontChanging) {
  174. flushCurrentRun = true;
  175. }
  176. if (flushCurrentRun) {
  177. //Paint the current run and reset for the next run
  178. psRun.paint(ps, textUtil, tpi);
  179. psRun.reset();
  180. }
  181. //Track current run
  182. psRun.addCharacter(paintChar, relPos);
  183. psRun.noteStartingTransformation(localTransform);
  184. //Change font if necessary
  185. if (fontChanging) {
  186. textUtil.setCurrentFont(f, mapped);
  187. }
  188. //Update last position
  189. prevPos = glyphPos;
  190. }
  191. psRun.paint(ps, textUtil, tpi);
  192. gen.writeln("ET"); //endTextObject()
  193. gen.restoreGraphicsState();
  194. if (DEBUG) {
  195. //Paint debug shapes
  196. g2d.setStroke(new BasicStroke(0));
  197. g2d.setColor(Color.LIGHT_GRAY);
  198. g2d.draw(debugShapes);
  199. }
  200. }
  201. private void applyColor(Paint paint, final PSGenerator gen) throws IOException {
  202. if (paint == null) {
  203. return;
  204. } else if (paint instanceof Color) {
  205. Color col = (Color)paint;
  206. gen.useColor(col);
  207. } else {
  208. log.warn("Paint not supported: " + paint.toString());
  209. }
  210. }
  211. private PSFontResource getResourceForFont(Font f, String postfix) {
  212. String key = (postfix != null ? f.getFontName() + '_' + postfix : f.getFontName());
  213. return this.fontResources.getFontResourceForFontKey(key);
  214. }
  215. private void clip(PSGraphics2D ps, Shape shape) throws IOException {
  216. if (shape == null) {
  217. return;
  218. }
  219. ps.getPSGenerator().writeln("newpath");
  220. PathIterator iter = shape.getPathIterator(IDENTITY_TRANSFORM);
  221. ps.processPathIterator(iter);
  222. ps.getPSGenerator().writeln("clip");
  223. }
  224. private class TextUtil {
  225. private PSGenerator gen;
  226. private Font[] fonts;
  227. private Font currentFont;
  228. private int currentEncoding = -1;
  229. public TextUtil(PSGenerator gen) {
  230. this.gen = gen;
  231. }
  232. public Font selectFontForChar(char ch) {
  233. for (int i = 0, c = fonts.length; i < c; i++) {
  234. if (fonts[i].hasChar(ch)) {
  235. return fonts[i];
  236. }
  237. }
  238. return fonts[0]; //TODO Maybe fall back to painting with shapes
  239. }
  240. public void writeTextMatrix(AffineTransform transform) throws IOException {
  241. double[] matrix = new double[6];
  242. transform.getMatrix(matrix);
  243. gen.writeln(gen.formatDouble5(matrix[0]) + " "
  244. + gen.formatDouble5(matrix[1]) + " "
  245. + gen.formatDouble5(matrix[2]) + " "
  246. + gen.formatDouble5(matrix[3]) + " "
  247. + gen.formatDouble5(matrix[4]) + " "
  248. + gen.formatDouble5(matrix[5]) + " Tm");
  249. }
  250. public boolean isFontChanging(Font f, char mapped) {
  251. if (f != getCurrentFont()) {
  252. return true;
  253. }
  254. if (mapped / 256 != getCurrentFontEncoding()) {
  255. return true;
  256. }
  257. return false; //Font is the same
  258. }
  259. public void selectFont(Font f, char mapped) throws IOException {
  260. int encoding = mapped / 256;
  261. String postfix = (encoding == 0 ? null : Integer.toString(encoding));
  262. PSFontResource res = getResourceForFont(f, postfix);
  263. gen.useFont("/" + res.getName(), f.getFontSize() / 1000f);
  264. res.notifyResourceUsageOnPage(gen.getResourceTracker());
  265. }
  266. public Font getCurrentFont() {
  267. return this.currentFont;
  268. }
  269. public int getCurrentFontEncoding() {
  270. return this.currentEncoding;
  271. }
  272. public void setCurrentFont(Font font, int encoding) {
  273. this.currentFont = font;
  274. this.currentEncoding = encoding;
  275. }
  276. public void setCurrentFont(Font font, char mapped) {
  277. int encoding = mapped / 256;
  278. setCurrentFont(font, encoding);
  279. }
  280. public void setupFonts(AttributedCharacterIterator runaci) {
  281. this.fonts = findFonts(runaci);
  282. }
  283. public boolean hasFonts() {
  284. return (fonts != null) && (fonts.length > 0);
  285. }
  286. }
  287. private class PSTextRun {
  288. private AffineTransform textTransform;
  289. private List relativePositions = new java.util.LinkedList();
  290. private StringBuffer currentChars = new StringBuffer();
  291. private int horizChanges = 0;
  292. private int vertChanges = 0;
  293. public void reset() {
  294. textTransform = null;
  295. currentChars.setLength(0);
  296. horizChanges = 0;
  297. vertChanges = 0;
  298. relativePositions.clear();
  299. }
  300. public int getHorizRunLength() {
  301. if (this.vertChanges == 0
  302. && getRunLength() > 0) {
  303. return getRunLength();
  304. }
  305. return 0;
  306. }
  307. public void addCharacter(char paintChar, Point2D relPos) {
  308. addRelativePosition(relPos);
  309. currentChars.append(paintChar);
  310. }
  311. private void addRelativePosition(Point2D relPos) {
  312. if (getRunLength() > 0) {
  313. if (relPos.getX() != 0) {
  314. horizChanges++;
  315. }
  316. if (relPos.getY() != 0) {
  317. vertChanges++;
  318. }
  319. }
  320. relativePositions.add(relPos);
  321. }
  322. public void noteStartingTransformation(AffineTransform transform) {
  323. if (textTransform == null) {
  324. this.textTransform = new AffineTransform(transform);
  325. }
  326. }
  327. public int getRunLength() {
  328. return currentChars.length();
  329. }
  330. private boolean isXShow() {
  331. return vertChanges == 0;
  332. }
  333. private boolean isYShow() {
  334. return horizChanges == 0;
  335. }
  336. public void paint(PSGraphics2D g2d, TextUtil textUtil, TextPaintInfo tpi)
  337. throws IOException {
  338. if (getRunLength() > 0) {
  339. if (log.isDebugEnabled()) {
  340. log.debug("Text run: " + currentChars);
  341. }
  342. textUtil.writeTextMatrix(this.textTransform);
  343. if (isXShow()) {
  344. log.debug("Horizontal text: xshow");
  345. paintXYShow(g2d, textUtil, tpi.fillPaint, true, false);
  346. } else if (isYShow()) {
  347. log.debug("Vertical text: yshow");
  348. paintXYShow(g2d, textUtil, tpi.fillPaint, false, true);
  349. } else {
  350. log.debug("Arbitrary text: xyshow");
  351. paintXYShow(g2d, textUtil, tpi.fillPaint, true, true);
  352. }
  353. boolean stroke = (tpi.strokePaint != null) && (tpi.strokeStroke != null);
  354. if (stroke) {
  355. log.debug("Stroked glyph outlines");
  356. paintStrokedGlyphs(g2d, textUtil, tpi.strokePaint, tpi.strokeStroke);
  357. }
  358. }
  359. }
  360. private void paintXYShow(PSGraphics2D g2d, TextUtil textUtil, Paint paint,
  361. boolean x, boolean y) throws IOException {
  362. PSGenerator gen = textUtil.gen;
  363. char firstChar = this.currentChars.charAt(0);
  364. //Font only has to be setup up before the first character
  365. Font f = textUtil.selectFontForChar(firstChar);
  366. char mapped = f.mapChar(firstChar);
  367. textUtil.selectFont(f, mapped);
  368. textUtil.setCurrentFont(f, mapped);
  369. applyColor(paint, gen);
  370. FontMetrics metrics = f.getFontMetrics();
  371. boolean multiByte = metrics instanceof MultiByteFont
  372. || metrics instanceof LazyFont
  373. && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont;
  374. StringBuffer sb = new StringBuffer();
  375. sb.append(multiByte ? '<' : '(');
  376. for (int i = 0, c = this.currentChars.length(); i < c; i++) {
  377. char ch = this.currentChars.charAt(i);
  378. mapped = f.mapChar(ch);
  379. if (multiByte) {
  380. sb.append(HexEncoder.encode(mapped));
  381. } else {
  382. char codepoint = (char) (mapped % 256);
  383. PSGenerator.escapeChar(codepoint, sb);
  384. }
  385. }
  386. sb.append(multiByte ? '>' : ')');
  387. if (x || y) {
  388. sb.append("\n[");
  389. int idx = 0;
  390. Iterator iter = this.relativePositions.iterator();
  391. while (iter.hasNext()) {
  392. Point2D pt = (Point2D)iter.next();
  393. if (idx > 0) {
  394. if (x) {
  395. sb.append(format(gen, pt.getX()));
  396. }
  397. if (y) {
  398. if (x) {
  399. sb.append(' ');
  400. }
  401. sb.append(format(gen, -pt.getY()));
  402. }
  403. if (idx % 8 == 0) {
  404. sb.append('\n');
  405. } else {
  406. sb.append(' ');
  407. }
  408. }
  409. idx++;
  410. }
  411. if (x) {
  412. sb.append('0');
  413. }
  414. if (y) {
  415. if (x) {
  416. sb.append(' ');
  417. }
  418. sb.append('0');
  419. }
  420. sb.append(']');
  421. }
  422. sb.append(' ');
  423. if (x) {
  424. sb.append('x');
  425. }
  426. if (y) {
  427. sb.append('y');
  428. }
  429. sb.append("show"); // --> xshow, yshow or xyshow
  430. gen.writeln(sb.toString());
  431. }
  432. private String format(PSGenerator gen, double coord) {
  433. if (Math.abs(coord) < 0.00001) {
  434. return "0";
  435. } else {
  436. return gen.formatDouble5(coord);
  437. }
  438. }
  439. private void paintStrokedGlyphs(PSGraphics2D g2d, TextUtil textUtil,
  440. Paint strokePaint, Stroke stroke) throws IOException {
  441. PSGenerator gen = textUtil.gen;
  442. applyColor(strokePaint, gen);
  443. PSGraphics2D.applyStroke(stroke, gen);
  444. Font f = null;
  445. Iterator iter = this.relativePositions.iterator();
  446. iter.next();
  447. Point2D pos = new Point2D.Double(0, 0);
  448. gen.writeln("0 0 M");
  449. for (int i = 0, c = this.currentChars.length(); i < c; i++) {
  450. char ch = this.currentChars.charAt(0);
  451. if (i == 0) {
  452. //Font only has to be setup up before the first character
  453. f = textUtil.selectFontForChar(ch);
  454. }
  455. char mapped = f.mapChar(ch);
  456. if (i == 0) {
  457. textUtil.selectFont(f, mapped);
  458. textUtil.setCurrentFont(f, mapped);
  459. }
  460. //add glyph outlines to current path
  461. mapped = f.mapChar(this.currentChars.charAt(i));
  462. FontMetrics metrics = f.getFontMetrics();
  463. boolean multiByte = metrics instanceof MultiByteFont
  464. || metrics instanceof LazyFont
  465. && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont;
  466. if (multiByte) {
  467. gen.write('<');
  468. gen.write(HexEncoder.encode(mapped));
  469. gen.write('>');
  470. } else {
  471. char codepoint = (char)(mapped % 256);
  472. gen.write("(" + codepoint + ")");
  473. }
  474. gen.writeln(" false charpath");
  475. if (iter.hasNext()) {
  476. //Position for the next character
  477. Point2D pt = (Point2D)iter.next();
  478. pos.setLocation(pos.getX() + pt.getX(), pos.getY() - pt.getY());
  479. gen.writeln(gen.formatDouble5(pos.getX()) + " "
  480. + gen.formatDouble5(pos.getY()) + " M");
  481. }
  482. }
  483. gen.writeln("stroke"); //paints all accumulated glyph outlines
  484. }
  485. }
  486. }