/*
 * 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: NativeTextPainter.java 1805173 2017-08-16 10:50:04Z ssteiner $ */

package org.apache.fop.svg;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.batik.bridge.FontFamilyResolver;
import org.apache.batik.bridge.GlyphLayout;
import org.apache.batik.bridge.SVGGVTFont;
import org.apache.batik.bridge.StrokingTextPainter;
import org.apache.batik.bridge.TextLayoutFactory;
import org.apache.batik.bridge.TextNode;
import org.apache.batik.bridge.TextSpanLayout;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.text.TextPaintInfo;

import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.svg.font.FOPFontFamilyResolverImpl;
import org.apache.fop.svg.font.FOPGVTFont;
import org.apache.fop.svg.font.FOPGVTGlyphVector;
import org.apache.fop.svg.text.BidiAttributedCharacterIterator;
import org.apache.fop.svg.text.ComplexGlyphLayout;
import org.apache.fop.util.CharUtilities;

Abstract base class for text painters that use specialized text commands native to an output format to render text.
/** * Abstract base class for text painters that use specialized text commands native to an output * format to render text. */
public abstract class NativeTextPainter extends StrokingTextPainter {
the logger for this class
/** the logger for this class */
protected static final Log log = LogFactory.getLog(NativeTextPainter.class); private static final boolean DEBUG = false;
the font collection
/** the font collection */
protected final FontInfo fontInfo; protected final FontFamilyResolver fontFamilyResolver; protected Font font; protected TextPaintInfo tpi;
Creates a new instance.
Params:
  • fontInfo – the font collection
/** * Creates a new instance. * @param fontInfo the font collection */
public NativeTextPainter(FontInfo fontInfo) { this.fontInfo = fontInfo; this.fontFamilyResolver = new FOPFontFamilyResolverImpl(fontInfo); }
Indicates whether the given Graphics2D instance if compatible with this text painter implementation.
Params:
  • g2d – the instance to check
Returns:true if the instance is compatible.
/** * Indicates whether the given {@link Graphics2D} instance if compatible with this text painter * implementation. * @param g2d the instance to check * @return true if the instance is compatible. */
protected abstract boolean isSupported(Graphics2D g2d);
Paints a single text run.
Params:
  • textRun – the text run
  • g2d – the target Graphics2D instance
Throws:
  • IOException – if an I/O error occurs while rendering the text
/** * Paints a single text run. * @param textRun the text run * @param g2d the target Graphics2D instance * @throws IOException if an I/O error occurs while rendering the text */
protected final void paintTextRun(TextRun textRun, Graphics2D g2d) throws IOException { logTextRun(textRun); AttributedCharacterIterator runaci = textRun.getACI(); runaci.first(); tpi = (TextPaintInfo) runaci.getAttribute(PAINT_INFO); if (tpi == null || !tpi.visible) { return; } if (tpi.composite != null) { g2d.setComposite(tpi.composite); } TextSpanLayout layout = textRun.getLayout(); GVTGlyphVector gv = layout.getGlyphVector(); if (!(gv.getFont() instanceof FOPGVTFont)) { assert gv.getFont() == null || gv.getFont() instanceof SVGGVTFont; //Draw using Java2D when no native fonts are available textRun.getLayout().draw(g2d); return; } else { GeneralPath debugShapes = new GeneralPath(); preparePainting(g2d); saveGraphicsState(); setInitialTransform(g2d.getTransform()); clip(g2d.getClip()); beginTextObject(); writeGlyphs((FOPGVTGlyphVector) gv, debugShapes); endTextObject(); restoreGraphicsState(); if (DEBUG) { g2d.setStroke(new BasicStroke(0)); g2d.setColor(Color.LIGHT_GRAY); g2d.draw(debugShapes); } } } protected void writeGlyphs(FOPGVTGlyphVector gv, GeneralPath debugShapes) throws IOException { AffineTransform localTransform = new AffineTransform(); Point2D prevPos = null; AffineTransform prevGlyphTransform = null; font = ((FOPGVTFont) gv.getFont()).getFont(); for (int index = 0, c = gv.getNumGlyphs(); index < c; index++) { if (!gv.isGlyphVisible(index)) { continue; } Point2D glyphPos = gv.getGlyphPosition(index); AffineTransform glyphTransform = gv.getGlyphTransform(index); if (log.isTraceEnabled()) { log.trace("pos " + glyphPos + ", transform " + glyphTransform); } if (DEBUG) { Shape sh = gv.getGlyphLogicalBounds(index); if (sh == null) { sh = new Ellipse2D.Double(glyphPos.getX(), glyphPos.getY(), 2, 2); } debugShapes.append(sh, false); } //Exact position of the glyph localTransform.setToIdentity(); localTransform.translate(glyphPos.getX(), glyphPos.getY()); if (glyphTransform != null) { localTransform.concatenate(glyphTransform); } localTransform.scale(1, -1); positionGlyph(prevPos, glyphPos, glyphTransform != null || prevGlyphTransform != null); char glyph = (char) gv.getGlyphCode(index); //Update last position prevPos = glyphPos; prevGlyphTransform = glyphTransform; writeGlyph(glyph, localTransform); } } @Override protected void paintTextRuns(@SuppressWarnings("rawtypes") List textRuns, Graphics2D g2d) { if (log.isTraceEnabled()) { log.trace("paintTextRuns: count = " + textRuns.size()); } if (!isSupported(g2d)) { super.paintTextRuns(textRuns, g2d); return; } for (Object textRun1 : textRuns) { TextRun textRun = (TextRun) textRun1; try { paintTextRun(textRun, g2d); } catch (IOException ioe) { //No other possibility than to use a RuntimeException throw new RuntimeException(ioe); } } }
Collects all characters from an AttributedCharacterIterator.
Params:
  • runaci – the character iterator
Returns:the characters
/** * Collects all characters from an {@link AttributedCharacterIterator}. * @param runaci the character iterator * @return the characters */
protected CharSequence collectCharacters(AttributedCharacterIterator runaci) { StringBuffer chars = new StringBuffer(); for (runaci.first(); runaci.getIndex() < runaci.getEndIndex();) { chars.append(runaci.current()); runaci.next(); } return chars; } // Use FOP's bidi algorithm implementation and sub-divide each chunk into runs // that respect bidi level boundaries. N.B. batik does not sub-divide chunks at // bidi level boundaries because it performs eager reordering. In FOP, we need // to perform lazy reordering after character to glyph mapping occurs since // that mapping process requires logical (not visual) ordered input. @Override public List computeTextRuns(TextNode node, AttributedCharacterIterator nodeACI, AttributedCharacterIterator [] chunkACIs) { nodeACI.first(); int defaultBidiLevel = (((Integer) nodeACI.getAttribute(WRITING_MODE)).intValue() == WRITING_MODE_RTL) ? 1 : 0; for (int i = 0, n = chunkACIs.length; i < n; ++i) { chunkACIs[i] = new BidiAttributedCharacterIterator(chunkACIs[i], defaultBidiLevel); } return super.computeTextRuns(node, nodeACI, chunkACIs, null); } // We want to sub-divide text chunks into distinct runs at bidi level boundaries. @Override protected Set getTextRunBoundaryAttributes() { Set textRunBoundaryAttributes = super.getTextRunBoundaryAttributes(); if (!textRunBoundaryAttributes.contains(BIDI_LEVEL)) { textRunBoundaryAttributes.add(BIDI_LEVEL); } return textRunBoundaryAttributes; } // Perform reordering of runs. @Override protected List reorderTextRuns(TextChunk chunk, List runs) { // 1. determine min/max bidi levels for runs int mn = -1; int mx = -1; for (TextRun r : (List<TextRun>) runs) { int level = r.getBidiLevel(); if (level >= 0) { if ((mn < 0) || (level < mn)) { mn = level; } if ((mx < 0) || (level > mx)) { mx = level; } } } // 2. reorder from maximum level to minimum odd level if (mx > 0) { for (int l1 = mx, l2 = ((mn & 1) == 0) ? (mn + 1) : mn; l1 >= l2; l1--) { runs = reorderRuns(runs, l1); } } // 3. reverse glyphs (and perform mirroring) in runs as needed boolean mirror = true; reverseGlyphs(runs, mirror); return runs; } private List reorderRuns(List runs, int level) { assert level >= 0; List runsNew = new java.util.ArrayList(); for (int i = 0, n = runs.size(); i < n; i++) { TextRun tri = (TextRun) runs.get(i); if (tri.getBidiLevel() < level) { runsNew.add(tri); } else { int s = i; int e = s; while (e < n) { TextRun tre = (TextRun) runs.get(e); if (tre.getBidiLevel() < level) { break; } else { e++; } } if (s < e) { runsNew.addAll(reverseRuns(runs, s, e)); } i = e - 1; } } if (!runsNew.equals(runs)) { runs = runsNew; } return runs; } private List reverseRuns(List runs, int s, int e) { int n = e - s; List runsNew = new java.util.ArrayList(n); if (n > 0) { for (int i = 0; i < n; i++) { int k = (n - i - 1); TextRun tr = (TextRun) runs.get(s + k); tr.reverse(); runsNew.add(tr); } } return runsNew; } private void reverseGlyphs(List runs, boolean mirror) { for (TextRun r : (List<TextRun>) runs) { r.maybeReverseGlyphs(mirror); } } protected abstract void preparePainting(Graphics2D g2d); protected abstract void saveGraphicsState() throws IOException; protected abstract void restoreGraphicsState() throws IOException; protected abstract void setInitialTransform(AffineTransform transform) throws IOException; protected abstract void clip(Shape clip) throws IOException; protected abstract void beginTextObject() throws IOException; protected abstract void endTextObject() throws IOException; protected abstract void positionGlyph(Point2D prevPos, Point2D glyphPos, boolean reposition); protected abstract void writeGlyph(char glyph, AffineTransform transform) throws IOException; protected final void logTextRun(TextRun textRun) { AttributedCharacterIterator runaci = textRun.getACI(); TextSpanLayout layout = textRun.getLayout(); runaci.first(); if (log.isTraceEnabled()) { int charCount = runaci.getEndIndex() - runaci.getBeginIndex(); log.trace("================================================"); log.trace("New text run:"); log.trace("char count: " + charCount); log.trace("range: " + runaci.getBeginIndex() + " - " + runaci.getEndIndex()); log.trace("glyph count: " + layout.getGlyphCount()); //=getNumGlyphs() } }
Params:
  • ch – a character
  • layout – a text span layout
  • index – an index
  • visibleChar – visible character flag
/** * @param ch a character * @param layout a text span layout * @param index an index * @param visibleChar visible character flag */
protected final void logCharacter(char ch, TextSpanLayout layout, int index, boolean visibleChar) { if (log.isTraceEnabled()) { log.trace("glyph " + index + " -> " + layout.getGlyphIndex(index) + " => " + ch); if (CharUtilities.isAnySpace(ch) && ch != 32) { log.trace("Space found: " + Integer.toHexString(ch)); } else if (ch == CharUtilities.ZERO_WIDTH_JOINER) { log.trace("ZWJ found: " + Integer.toHexString(ch)); } else if (ch == CharUtilities.SOFT_HYPHEN) { log.trace("Soft hyphen found: " + Integer.toHexString(ch)); } if (!visibleChar) { log.trace("Invisible glyph found: " + Integer.toHexString(ch)); } } } @Override protected FontFamilyResolver getFontFamilyResolver() { return this.fontFamilyResolver; } private static final TextLayoutFactory COMPLEX_SCRIPT_TEXT_LAYOUT_FACTORY = new TextLayoutFactory() { public TextSpanLayout createTextLayout(AttributedCharacterIterator aci, int [] charMap, Point2D offset, FontRenderContext frc) { if (ComplexGlyphLayout.mayRequireComplexLayout(aci)) { return new ComplexGlyphLayout(aci, charMap, offset, frc); } else { return new GlyphLayout(aci, charMap, offset, frc); } } }; @Override protected TextLayoutFactory getTextLayoutFactory() { return COMPLEX_SCRIPT_TEXT_LAYOUT_FACTORY; } }