/*
 * 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: FOPGVTGlyphVector.java 1831346 2018-05-10 14:30:38Z matthias $ */

package org.apache.fop.svg.font;

import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphJustificationInfo;
import java.awt.font.GlyphMetrics;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.text.AttributedCharacterIterator;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.Arrays;
import java.util.List;

import org.apache.batik.gvt.font.GVTFont;
import org.apache.batik.gvt.font.GVTGlyphMetrics;
import org.apache.batik.gvt.font.GVTGlyphVector;
import org.apache.batik.gvt.font.GVTLineMetrics;
import org.apache.batik.gvt.text.GVTAttributedCharacterIterator;

import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontMetrics;
import org.apache.fop.fonts.GlyphMapping;
import org.apache.fop.fonts.TextFragment;
import org.apache.fop.traits.MinOptMax;

public class FOPGVTGlyphVector implements GVTGlyphVector {

    protected final TextFragment text;

    protected final FOPGVTFont font;

    private final int fontSize;

    private final FontMetrics fontMetrics;

    private final FontRenderContext frc;

    protected int[] glyphs;

    protected List associations;

    protected int[][] gposAdjustments;

    protected float[] positions;

    protected Rectangle2D[] boundingBoxes;

    protected GeneralPath outline;

    protected AffineTransform[] glyphTransforms;

    protected boolean[] glyphVisibilities;

    protected Rectangle2D logicalBounds;

    FOPGVTGlyphVector(FOPGVTFont font, final CharacterIterator iter, FontRenderContext frc) {
        this.text = new SVGTextFragment(iter);
        this.font = font;
        Font f = font.getFont();
        this.fontSize = f.getFontSize();
        this.fontMetrics = f.getFontMetrics();
        this.frc = frc;
    }

    public void performDefaultLayout() {
        Font f = font.getFont();
        MinOptMax letterSpaceIPD = MinOptMax.ZERO;
        MinOptMax[] letterSpaceAdjustments = new MinOptMax[text.getEndIndex()];
        boolean retainControls = false;
        GlyphMapping mapping = GlyphMapping.doGlyphMapping(text, text.getBeginIndex(), text.getEndIndex(),
            f, letterSpaceIPD, letterSpaceAdjustments, '\0', '\0',
            false, text.getBidiLevel(), true, true, retainControls);
        CharacterIterator glyphAsCharIter =
            mapping.mapping != null ? new StringCharacterIterator(mapping.mapping) : text.getIterator();
        this.glyphs = buildGlyphs(f, glyphAsCharIter);
        this.associations = mapping.associations;
        this.gposAdjustments = mapping.gposAdjustments;
        if (text.getBeginIndex() > 0) {
            int arrlen = text.getEndIndex() - text.getBeginIndex();
            MinOptMax[] letterSpaceAdjustmentsNew = new MinOptMax[arrlen];
            System.arraycopy(letterSpaceAdjustments, text.getBeginIndex(), letterSpaceAdjustmentsNew,
                    0, arrlen);
            letterSpaceAdjustments = letterSpaceAdjustmentsNew;
        }
        this.positions = buildGlyphPositions(glyphAsCharIter, mapping.gposAdjustments, letterSpaceAdjustments);
        this.glyphVisibilities = new boolean[this.glyphs.length];
        Arrays.fill(glyphVisibilities, true);
        this.glyphTransforms = new AffineTransform[this.glyphs.length];
    }

    private static class SVGTextFragment implements TextFragment {

        private final CharacterIterator charIter;

        private String script;

        private String language;

        private int level = -1;

        SVGTextFragment(CharacterIterator charIter) {
            this.charIter = charIter;
            if (charIter instanceof AttributedCharacterIterator) {
                AttributedCharacterIterator aci = (AttributedCharacterIterator) charIter;
                aci.first();
                this.script = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.SCRIPT);
                this.language = (String) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.LANGUAGE);
                Integer level = (Integer) aci.getAttribute(GVTAttributedCharacterIterator.TextAttribute.BIDI_LEVEL);
                if (level != null) {
                    this.level = level;
                }
            }
        }

        public CharacterIterator getIterator() {
            return charIter;
        }

        public int getBeginIndex() {
            return charIter.getBeginIndex();
        }

        public int getEndIndex() {
            return charIter.getEndIndex();
        }

        // TODO - [GA] the following appears to be broken because it ignores
        // sttart and end index arguments
        public CharSequence subSequence(int startIndex, int endIndex) {
            StringBuilder sb = new StringBuilder();
            for (char c = charIter.first(); c != CharacterIterator.DONE; c = charIter.next()) {
                sb.append(c);
            }
            return sb.toString();
        }

        public String getScript() {
            if (script != null) {
                return script;
            } else {
                return "auto";
            }
        }

        public String getLanguage() {
            if (language != null) {
                return language;
            } else {
                return "none";
            }
        }

        public int getBidiLevel() {
            return level;
        }

        public char charAt(int index) {
            return charIter.setIndex(index - charIter.getBeginIndex());
        }
    }

    private int[] buildGlyphs(Font font, final CharacterIterator glyphAsCharIter) {
        int[] glyphs = new int[glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex()];
        int index = 0;
        for (char c = glyphAsCharIter.first();  c != CharacterIterator.DONE; c = glyphAsCharIter.next()) {
            glyphs[index] = font.mapChar(c);
            index++;
        }
        return glyphs;
    }

    private static final int[] PA_ZERO = new int[4];

    
Build glyph position array.
Params:
  • glyphAsCharIter – iterator for mapped glyphs as char codes (not glyph codes)
  • dp – optionally null glyph position adjustments array
  • lsa – optionally null letter space adjustments array
Returns:array of floats that denote [X,Y] position pairs for each glyph including including an implied subsequent glyph; i.e., returned array contains one more pair than the numbers of glyphs, where the position denoted by this last pair represents the position after the last glyph has incurred advancement
/** * Build glyph position array. * @param glyphAsCharIter iterator for mapped glyphs as char codes (not glyph codes) * @param dp optionally null glyph position adjustments array * @param lsa optionally null letter space adjustments array * @return array of floats that denote [X,Y] position pairs for each glyph including * including an implied subsequent glyph; i.e., returned array contains one more pair * than the numbers of glyphs, where the position denoted by this last pair represents * the position after the last glyph has incurred advancement */
private float[] buildGlyphPositions(final CharacterIterator glyphAsCharIter, int[][] dp, MinOptMax[] lsa) { int numGlyphs = glyphAsCharIter.getEndIndex() - glyphAsCharIter.getBeginIndex(); float[] positions = new float[2 * (numGlyphs + 1)]; float xc = 0f; float yc = 0f; if (dp != null) { for (int i = 0; i < numGlyphs + 1; ++i) { int[] pa = ((i >= dp.length) || (dp[i] == null)) ? PA_ZERO : dp[i]; float xo = xc + ((float) pa[0]) / 1000f; float yo = yc - ((float) pa[1]) / 1000f; float xa = getGlyphWidth(i) + ((float) pa[2]) / 1000f; float ya = ((float) pa[3]) / 1000f; int k = 2 * i; positions[k + 0] = xo; positions[k + 1] = yo; xc += xa; yc += ya; } } else if (lsa != null) { for (int i = 0; i < numGlyphs + 1; ++i) { MinOptMax sa = (((i + 1) >= lsa.length) || (lsa[i + 1] == null)) ? MinOptMax.ZERO : lsa[i + 1]; float xo = xc; float yo = yc; float xa = getGlyphWidth(i) + sa.getOpt() / 1000f; float ya = 0; int k = 2 * i; positions[k + 0] = xo; positions[k + 1] = yo; xc += xa; yc += ya; } } return positions; } private float getGlyphWidth(int index) { if (index < glyphs.length) { return fontMetrics.getWidth(glyphs[index], fontSize) / 1000000f; } else { return 0f; } } public GVTFont getFont() { return font; } public FontRenderContext getFontRenderContext() { return frc; } public void setGlyphCode(int glyphIndex, int glyphCode) { glyphs[glyphIndex] = glyphCode; } public int getGlyphCode(int glyphIndex) { return glyphs[glyphIndex]; } public int[] getGlyphCodes(int beginGlyphIndex, int numEntries, int[] codeReturn) { if (codeReturn == null) { codeReturn = new int[numEntries]; } System.arraycopy(glyphs, beginGlyphIndex, codeReturn, 0, numEntries); return codeReturn; } public GlyphJustificationInfo getGlyphJustificationInfo(int glyphIndex) { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } public Shape getGlyphLogicalBounds(int glyphIndex) { GVTGlyphMetrics metrics = getGlyphMetrics(glyphIndex); Point2D pos = getGlyphPosition(glyphIndex); GVTLineMetrics fontMetrics = font.getLineMetrics(0); Rectangle2D bounds = new Rectangle2D.Float(0, -fontMetrics.getDescent(), metrics.getHorizontalAdvance(), fontMetrics.getAscent() + fontMetrics.getDescent()); AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY()); AffineTransform transf = getGlyphTransform(glyphIndex); if (transf != null) { t.concatenate(transf); } t.scale(1, -1); // Translate from glyph coordinate system to user return t.createTransformedShape(bounds); } public GVTGlyphMetrics getGlyphMetrics(int glyphIndex) { Rectangle2D bbox = getBoundingBoxes()[glyphIndex]; return new GVTGlyphMetrics(positions[2 * (glyphIndex + 1)] - positions[2 * glyphIndex], (fontMetrics.getAscender(fontSize) - fontMetrics.getDescender(fontSize)) / 1000000f, bbox, GlyphMetrics.STANDARD); } public Shape getGlyphOutline(int glyphIndex) { Shape glyphBox = getBoundingBoxes()[glyphIndex]; AffineTransform tr = AffineTransform.getTranslateInstance(positions[glyphIndex * 2], positions[glyphIndex * 2 + 1]); AffineTransform glyphTransform = getGlyphTransform(glyphIndex); if (glyphTransform != null) { tr.concatenate(glyphTransform); } return tr.createTransformedShape(glyphBox); } public Rectangle2D getGlyphCellBounds(int glyphIndex) { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } public int[][] getGlyphPositionAdjustments() { return gposAdjustments; } public Point2D getGlyphPosition(int glyphIndex) { int positionIndex = glyphIndex * 2; return new Point2D.Float(positions[positionIndex], positions[positionIndex + 1]); } public float[] getGlyphPositions(int beginGlyphIndex, int numEntries, float[] positionReturn) { if (positionReturn == null) { positionReturn = new float[numEntries * 2]; } System.arraycopy(positions, beginGlyphIndex * 2, positionReturn, 0, numEntries * 2); return positionReturn; } public AffineTransform getGlyphTransform(int glyphIndex) { return glyphTransforms[glyphIndex]; } public Shape getGlyphVisualBounds(int glyphIndex) { Rectangle2D bbox = getBoundingBoxes()[glyphIndex]; Point2D pos = getGlyphPosition(glyphIndex); AffineTransform t = AffineTransform.getTranslateInstance(pos.getX(), pos.getY()); AffineTransform transf = getGlyphTransform(glyphIndex); if (transf != null) { t.concatenate(transf); } return t.createTransformedShape(bbox); } public Rectangle2D getLogicalBounds() { if (logicalBounds == null) { GeneralPath logicalBoundsPath = new GeneralPath(); for (int i = 0; i < getNumGlyphs(); i++) { Shape glyphLogicalBounds = getGlyphLogicalBounds(i); logicalBoundsPath.append(glyphLogicalBounds, false); } logicalBounds = logicalBoundsPath.getBounds2D(); } return logicalBounds; } public int getNumGlyphs() { return glyphs.length; } public Shape getOutline() { if (outline == null) { outline = new GeneralPath(); for (int i = 0; i < glyphs.length; i++) { outline.append(getGlyphOutline(i), false); } } return outline; } public Shape getOutline(float x, float y) { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } public Rectangle2D getGeometricBounds() { // TODO Auto-generated method stub throw new UnsupportedOperationException(); } public Rectangle2D getBounds2D(AttributedCharacterIterator aci) { return getOutline().getBounds2D(); } public void setGlyphPosition(int glyphIndex, Point2D newPos) { int idx = glyphIndex * 2; positions[idx] = (float) newPos.getX(); positions[idx + 1] = (float) newPos.getY(); } public void setGlyphTransform(int glyphIndex, AffineTransform newTX) { glyphTransforms[glyphIndex] = newTX; } public void setGlyphVisible(int glyphIndex, boolean visible) { glyphVisibilities[glyphIndex] = visible; } public boolean isGlyphVisible(int glyphIndex) { return glyphVisibilities[glyphIndex]; } public int getCharacterCount(int startGlyphIndex, int endGlyphIndex) { // TODO Not that simple if complex scripts are involved return endGlyphIndex - startGlyphIndex + 1; } public boolean isReversed() { return false; } public void maybeReverse(boolean mirror) { } public void draw(Graphics2D graphics2d, AttributedCharacterIterator aci) { // NOP } private Rectangle2D[] getBoundingBoxes() { if (boundingBoxes == null) { buildBoundingBoxes(); } return boundingBoxes; } private void buildBoundingBoxes() { boundingBoxes = new Rectangle2D[glyphs.length]; for (int i = 0; i < glyphs.length; i++) { Rectangle bbox = fontMetrics.getBoundingBox(glyphs[i], fontSize); boundingBoxes[i] = new Rectangle2D.Float(bbox.x / 1000000f, -(bbox.y + bbox.height) / 1000000f, bbox.width / 1000000f, bbox.height / 1000000f); } } }