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