| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.render.ps; |
| |
| import java.awt.Color; |
| import java.awt.Graphics2D; |
| import java.awt.Paint; |
| import java.awt.Shape; |
| import java.awt.Stroke; |
| import java.awt.geom.AffineTransform; |
| import java.awt.geom.PathIterator; |
| import java.awt.geom.Point2D; |
| import java.awt.geom.Point2D.Double; |
| import java.io.IOException; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| import org.apache.batik.gvt.text.TextPaintInfo; |
| |
| import org.apache.xmlgraphics.java2d.ps.PSGraphics2D; |
| import org.apache.xmlgraphics.ps.PSGenerator; |
| |
| import org.apache.fop.fonts.Font; |
| import org.apache.fop.fonts.FontInfo; |
| import org.apache.fop.fonts.FontMetrics; |
| import org.apache.fop.fonts.LazyFont; |
| import org.apache.fop.fonts.MultiByteFont; |
| import org.apache.fop.svg.NativeTextPainter; |
| import org.apache.fop.util.HexEncoder; |
| |
| /** |
| * Renders the attributed character iterator of a {@link org.apache.batik.bridge.TextNode TextNode}. |
| * This class draws the text directly using PostScript text operators so |
| * the text is not drawn using shapes which makes the PS files larger. |
| * <p> |
| * The text runs are split into smaller text runs that can be bundles in single |
| * calls of the xshow, yshow or xyshow operators. For outline text, the charpath |
| * operator is used. |
| */ |
| public class PSTextPainter extends NativeTextPainter { |
| |
| private FontResourceCache fontResources; |
| |
| private PSGraphics2D ps; |
| |
| private PSGenerator gen; |
| |
| private TextUtil textUtil; |
| |
| private boolean flushCurrentRun; |
| |
| private PSTextRun psRun; |
| |
| private Double relPos; |
| |
| private static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); |
| |
| /** |
| * Create a new PS text painter with the given font information. |
| * @param fontInfo the font collection |
| */ |
| public PSTextPainter(FontInfo fontInfo) { |
| super(fontInfo); |
| this.fontResources = new FontResourceCache(fontInfo); |
| } |
| |
| /** {@inheritDoc} */ |
| protected boolean isSupported(Graphics2D g2d) { |
| return g2d instanceof PSGraphics2D; |
| } |
| |
| @Override |
| protected void preparePainting(Graphics2D g2d) { |
| ps = (PSGraphics2D) g2d; |
| gen = ps.getPSGenerator(); |
| ps.preparePainting(); |
| } |
| |
| @Override |
| protected void saveGraphicsState() throws IOException { |
| gen.saveGraphicsState(); |
| } |
| |
| @Override |
| protected void restoreGraphicsState() throws IOException { |
| gen.restoreGraphicsState(); |
| } |
| |
| @Override |
| protected void setInitialTransform(AffineTransform transform) throws IOException { |
| gen.concatMatrix(transform); |
| } |
| |
| private PSFontResource getResourceForFont(Font f, String postfix) { |
| String key = (postfix != null ? f.getFontName() + '_' + postfix : f.getFontName()); |
| return this.fontResources.getFontResourceForFontKey(key); |
| } |
| |
| @Override |
| protected void clip(Shape shape) throws IOException { |
| if (shape == null) { |
| return; |
| } |
| ps.getPSGenerator().writeln("newpath"); |
| PathIterator iter = shape.getPathIterator(IDENTITY_TRANSFORM); |
| ps.processPathIterator(iter); |
| ps.getPSGenerator().writeln("clip"); |
| } |
| |
| @Override |
| protected void beginTextObject() throws IOException { |
| gen.writeln("BT"); |
| textUtil = new TextUtil(); |
| psRun = new PSTextRun(); //Used to split a text run into smaller runs |
| } |
| |
| @Override |
| protected void endTextObject() throws IOException { |
| psRun.paint(ps, textUtil, tpi); |
| gen.writeln("ET"); |
| } |
| |
| @Override |
| protected void positionGlyph(Point2D prevPos, Point2D glyphPos, boolean reposition) { |
| flushCurrentRun = false; |
| //Try to optimize by combining characters using the same font and on the same line. |
| if (reposition) { |
| //Happens for text-on-a-path |
| flushCurrentRun = true; |
| } |
| if (psRun.getRunLength() >= 128) { |
| //Don't let a run get too long |
| flushCurrentRun = true; |
| } |
| |
| //Note the position of the glyph relative to the previous one |
| if (prevPos == null) { |
| relPos = new Point2D.Double(0, 0); |
| } else { |
| relPos = new Point2D.Double( |
| glyphPos.getX() - prevPos.getX(), |
| glyphPos.getY() - prevPos.getY()); |
| } |
| if (psRun.vertChanges == 0 |
| && psRun.getHorizRunLength() > 2 |
| && relPos.getY() != 0) { |
| //new line |
| flushCurrentRun = true; |
| } |
| } |
| |
| @Override |
| protected void writeGlyph(char glyph, AffineTransform localTransform) throws IOException { |
| boolean fontChanging = textUtil.isFontChanging(font, glyph); |
| if (fontChanging) { |
| flushCurrentRun = true; |
| } |
| |
| if (flushCurrentRun) { |
| //Paint the current run and reset for the next run |
| psRun.paint(ps, textUtil, tpi); |
| psRun.reset(); |
| } |
| |
| //Track current run |
| psRun.addGlyph(glyph, relPos); |
| psRun.noteStartingTransformation(localTransform); |
| |
| //Change font if necessary |
| if (fontChanging) { |
| textUtil.setCurrentFont(font, glyph); |
| } |
| } |
| |
| private class TextUtil { |
| |
| private Font currentFont; |
| private int currentEncoding = -1; |
| |
| public boolean isMultiByte(Font f) { |
| FontMetrics metrics = f.getFontMetrics(); |
| boolean multiByte = metrics instanceof MultiByteFont || metrics instanceof LazyFont |
| && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont; |
| return multiByte; |
| } |
| |
| public void writeTextMatrix(AffineTransform transform) throws IOException { |
| double[] matrix = new double[6]; |
| transform.getMatrix(matrix); |
| gen.writeln(gen.formatDouble5(matrix[0]) + " " |
| + gen.formatDouble5(matrix[1]) + " " |
| + gen.formatDouble5(matrix[2]) + " " |
| + gen.formatDouble5(matrix[3]) + " " |
| + gen.formatDouble5(matrix[4]) + " " |
| + gen.formatDouble5(matrix[5]) + " Tm"); |
| } |
| |
| public boolean isFontChanging(Font f, char mapped) { |
| // this is only applicable for single byte fonts |
| if (!isMultiByte(f)) { |
| if (f != getCurrentFont()) { |
| return true; |
| } |
| if (mapped / 256 != getCurrentFontEncoding()) { |
| return true; |
| } |
| } |
| return false; //Font is the same |
| } |
| |
| public void selectFont(Font f, char mapped) throws IOException { |
| int encoding = mapped / 256; |
| String postfix = (!isMultiByte(f) && encoding > 0 ? Integer.toString(encoding) : null); |
| PSFontResource res = getResourceForFont(f, postfix); |
| gen.useFont("/" + res.getName(), f.getFontSize() / 1000f); |
| res.notifyResourceUsageOnPage(gen.getResourceTracker()); |
| } |
| |
| public Font getCurrentFont() { |
| return this.currentFont; |
| } |
| |
| public int getCurrentFontEncoding() { |
| return this.currentEncoding; |
| } |
| |
| public void setCurrentFont(Font font, int encoding) { |
| this.currentFont = font; |
| this.currentEncoding = encoding; |
| } |
| |
| public void setCurrentFont(Font font, char mapped) { |
| int encoding = mapped / 256; |
| setCurrentFont(font, encoding); |
| } |
| |
| } |
| |
| private class PSTextRun { |
| |
| private AffineTransform textTransform; |
| private List<Point2D> relativePositions = new LinkedList<Point2D>(); |
| private StringBuffer currentGlyphs = new StringBuffer(); |
| private int horizChanges; |
| private int vertChanges; |
| |
| public void reset() { |
| textTransform = null; |
| currentGlyphs.setLength(0); |
| horizChanges = 0; |
| vertChanges = 0; |
| relativePositions.clear(); |
| } |
| |
| public int getHorizRunLength() { |
| if (this.vertChanges == 0 |
| && getRunLength() > 0) { |
| return getRunLength(); |
| } |
| return 0; |
| } |
| |
| public void addGlyph(char glyph, Point2D relPos) { |
| addRelativePosition(relPos); |
| currentGlyphs.append(glyph); |
| } |
| |
| private void addRelativePosition(Point2D relPos) { |
| if (getRunLength() > 0) { |
| if (relPos.getX() != 0) { |
| horizChanges++; |
| } |
| if (relPos.getY() != 0) { |
| vertChanges++; |
| } |
| } |
| relativePositions.add(relPos); |
| } |
| |
| public void noteStartingTransformation(AffineTransform transform) { |
| if (textTransform == null) { |
| this.textTransform = new AffineTransform(transform); |
| } |
| } |
| |
| public int getRunLength() { |
| return currentGlyphs.length(); |
| } |
| |
| private boolean isXShow() { |
| return vertChanges == 0; |
| } |
| |
| private boolean isYShow() { |
| return horizChanges == 0; |
| } |
| |
| public void paint(PSGraphics2D g2d, TextUtil textUtil, TextPaintInfo tpi) |
| throws IOException { |
| if (getRunLength() > 0) { |
| textUtil.writeTextMatrix(this.textTransform); |
| if (isXShow()) { |
| log.debug("Horizontal text: xshow"); |
| paintXYShow(g2d, textUtil, tpi.fillPaint, true, false); |
| } else if (isYShow()) { |
| log.debug("Vertical text: yshow"); |
| paintXYShow(g2d, textUtil, tpi.fillPaint, false, true); |
| } else { |
| log.debug("Arbitrary text: xyshow"); |
| paintXYShow(g2d, textUtil, tpi.fillPaint, true, true); |
| } |
| boolean stroke = (tpi.strokePaint != null) && (tpi.strokeStroke != null); |
| if (stroke) { |
| log.debug("Stroked glyph outlines"); |
| paintStrokedGlyphs(g2d, textUtil, tpi.strokePaint, tpi.strokeStroke); |
| } |
| } |
| } |
| |
| private void paintXYShow(PSGraphics2D g2d, TextUtil textUtil, Paint paint, |
| boolean x, boolean y) throws IOException { |
| char glyph = currentGlyphs.charAt(0); |
| textUtil.selectFont(font, glyph); |
| textUtil.setCurrentFont(font, glyph); |
| applyColor(paint); |
| |
| boolean multiByte = textUtil.isMultiByte(font); |
| StringBuffer sb = new StringBuffer(); |
| sb.append(multiByte ? '<' : '('); |
| for (int i = 0, c = this.currentGlyphs.length(); i < c; i++) { |
| glyph = this.currentGlyphs.charAt(i); |
| if (multiByte) { |
| sb.append(HexEncoder.encode(glyph)); |
| } else { |
| char codepoint = (char) (glyph % 256); |
| PSGenerator.escapeChar(codepoint, sb); |
| } |
| } |
| sb.append(multiByte ? '>' : ')'); |
| if (x || y) { |
| sb.append("\n["); |
| int idx = 0; |
| for (Point2D pt : this.relativePositions) { |
| if (idx > 0) { |
| if (x) { |
| sb.append(format(gen, pt.getX())); |
| } |
| if (y) { |
| if (x) { |
| sb.append(' '); |
| } |
| sb.append(format(gen, -pt.getY())); |
| } |
| if (idx % 8 == 0) { |
| sb.append('\n'); |
| } else { |
| sb.append(' '); |
| } |
| } |
| idx++; |
| } |
| if (x) { |
| sb.append('0'); |
| } |
| if (y) { |
| if (x) { |
| sb.append(' '); |
| } |
| sb.append('0'); |
| } |
| sb.append(']'); |
| } |
| sb.append(' '); |
| if (x) { |
| sb.append('x'); |
| } |
| if (y) { |
| sb.append('y'); |
| } |
| sb.append("show"); // --> xshow, yshow or xyshow |
| gen.writeln(sb.toString()); |
| } |
| |
| private void applyColor(Paint paint) throws IOException { |
| if (paint == null) { |
| return; |
| } else if (paint instanceof Color) { |
| Color col = (Color) paint; |
| gen.useColor(col); |
| } else { |
| log.warn("Paint not supported: " + paint.toString()); |
| } |
| } |
| |
| private String format(PSGenerator gen, double coord) { |
| if (Math.abs(coord) < 0.00001) { |
| return "0"; |
| } else { |
| return gen.formatDouble5(coord); |
| } |
| } |
| |
| private void paintStrokedGlyphs(PSGraphics2D g2d, TextUtil textUtil, |
| Paint strokePaint, Stroke stroke) throws IOException { |
| if (currentGlyphs.toString().trim().isEmpty()) { |
| return; |
| } |
| applyColor(strokePaint); |
| PSGraphics2D.applyStroke(stroke, gen); |
| |
| Iterator<Point2D> iter = this.relativePositions.iterator(); |
| iter.next(); |
| Point2D pos = new Point2D.Double(0, 0); |
| gen.writeln("0 0 M"); |
| for (int i = 0, c = this.currentGlyphs.length(); i < c; i++) { |
| char mapped = this.currentGlyphs.charAt(i); |
| if (i == 0) { |
| textUtil.selectFont(font, mapped); |
| textUtil.setCurrentFont(font, mapped); |
| } |
| //add glyph outlines to current path |
| FontMetrics metrics = font.getFontMetrics(); |
| boolean multiByte = metrics instanceof MultiByteFont |
| || metrics instanceof LazyFont |
| && ((LazyFont) metrics).getRealFont() instanceof MultiByteFont; |
| if (multiByte) { |
| gen.write("<"); |
| gen.write(HexEncoder.encode(mapped)); |
| gen.write(">"); |
| } else { |
| char codepoint = (char) (mapped % 256); |
| gen.write("(" + codepoint + ")"); |
| } |
| gen.writeln(" false charpath"); |
| |
| if (iter.hasNext()) { |
| //Position for the next character |
| Point2D pt = iter.next(); |
| pos.setLocation(pos.getX() + pt.getX(), pos.getY() - pt.getY()); |
| gen.writeln(gen.formatDouble5(pos.getX()) + " " |
| + gen.formatDouble5(pos.getY()) + " M"); |
| } |
| } |
| gen.writeln("stroke"); //paints all accumulated glyph outlines |
| } |
| |
| } |
| |
| } |
| |
| |