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