/*
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * "The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (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.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License.
 *
 * The Original Code is ICEpdf 3.0 open source software code, released
 * May 1st, 2009. The Initial Developer of the Original Code is ICEsoft
 * Technologies Canada, Corp. Portions created by ICEsoft are Copyright (C)
 * 2004-2009 ICEsoft Technologies Canada, Corp. All Rights Reserved.
 *
 * Contributor(s): _____________________.
 *
 * Alternatively, the contents of this file may be used under the terms of
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"
 * License), in which case the provisions of the LGPL License are
 * applicable instead of those above. If you wish to allow use of your
 * version of this file only under the terms of the LGPL License and not to
 * allow others to use your version of this file under the MPL, indicate
 * your decision by deleting the provisions above and replace them with
 * the notice and other provisions required by the LGPL License. If you do
 * not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the LGPL License."
 *
 */
package com.softwareimaging.jpdf.core.pobjects.fonts.ofont;

import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.Region.Op;

import com.softwareimaging.android.print.Printer;
import com.softwareimaging.android.graphics.AffineTransform;
import com.softwareimaging.android.graphics.Canvas;
import com.softwareimaging.android.graphics.Color;
import com.softwareimaging.android.graphics.Paint;
import com.softwareimaging.jpdf.core.pobjects.fonts.CMap;
import com.softwareimaging.jpdf.core.pobjects.fonts.FontFile;
import com.softwareimaging.jpdf.core.pobjects.PRectangle;
import com.softwareimaging.geometry.RectangleF;
import com.softwareimaging.geometry.SizeF;
import com.softwareimaging.graphics.IFont;
import com.softwareimaging.graphics.IFont.EncodingType;
import com.softwareimaging.graphics.IFont.FontType;
import com.softwareimaging.fonts.IFIMetrics;
import com.softwareimaging.fonts.truetype.CharSet;
import com.softwareimaging.fonts.truetype.TTFontParser;
import com.softwareimaging.fonts.truetype.TTcmap;
import com.softwareimaging.exceptions.ArgumentRangeException;
import com.softwareimaging.exceptions.TTFontParserException;
import com.softwareimaging.jpdf.core.pobjects.graphics.Shapes;
import com.softwareimaging.jpdf.core.util.PDFPrintException;
import com.softwareimaging.jpdf.util.HashMap;
import com.softwareimaging.logger.Logger;
import com.softwareimaging.print.CharProc;
import com.softwareimaging.data.ILogicalStreamReader;

import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

/**
 * OFont is an awt Font wrapper used to aid in the paint of glyphs.
 * 
 * @since 3.0
 */
public class OFont implements FontFile {

	private String familyName;
	private String faceName;
	private byte pitch;
	private float textSize;
	private byte charSet;
	private byte selection;
	private byte fontFamily;
	private int weight;

	// Embedded font data class
	class EmbeddedFontData {
		ILogicalStreamReader reader;
		int[] fontStreamLengths;
		public byte fontType;
		public byte encodingType;
		final CharSet cs = new CharSet();

		EmbeddedFontData(final ILogicalStreamReader reader, final int[] fontStreamLengths,
				final byte fontType, final byte encodingType, final Vector<?> charNameStrings) {
			this.reader = reader;
			this.fontStreamLengths = fontStreamLengths;
			this.fontType = fontType;
			this.encodingType = encodingType;
			cs.glyphNames = charNameStrings;
		}

		EmbeddedFontData(EmbeddedFontData fd) {
			this.reader = fd.reader;
			this.fontStreamLengths = fd.fontStreamLengths;
			this.fontType = fd.fontType;
			this.encodingType = fd.encodingType;
			cs.glyphNames = fd.cs.glyphNames;
			cs.toUnicode = fd.cs.toUnicode;
		}
	}

	// Embedded font data or Visual font
	private Object fontObj;

	// Member variables that do not impact realizing the font
	private float ascent;
	private float descent;
	private float[] widths;

	/**
	 * Transform mapping glyph space to text space. This is only used by Type 3
	 * fonts because other fonts are described in a 1000-unit glyph coordinate
	 * system, in which case the transform is [0.001 0 0 0.001 0 0].
	 */
	private AffineTransform transform;

	private Printer printer;
	private IFont iFont;
	private Map<Integer, Float> cidWidths;
	private Float cidDefaultWidth;
	private Map<Integer, CIDVerticalMetrics> cidVMetrics;
	private CIDVerticalMetrics cidDefaultVMetrics;
	private float missingWidth;
	private int firstCh;
	/**
	 * map from char code to Unicode
	 */
	private CMap toUnicode;
	/**
	 * map from char code to char code
	 */
	private char[] cMap;
	/**
	 * map from cid to glyph index
	 */
	private byte[] CIDToGIDMap;
	/**
	 * map from char code to cid
	 */
	private CMap toCID;
	/**
	 * map from cid to Unicode
	 */
	private CMap CIDToUnicode;
	/**
	 * map from char code to glyph index
	 */
	private char[] toGIDMap;
	private PRectangle fontBBox;

	private static final float defaultSize = 12.0f;

	// text layout map, very expensive to create, so we'll cache them.
	private final HashMap<String, PointF> echarAdvanceCache = new HashMap<String, PointF>();

	public OFont(Printer printer, String faceName, String family, int style, byte charSet,
			int weight, boolean fixedPitch, boolean script) {
		if (printer == null) {
			throw new NullPointerException();
		}

		setFaceName(faceName);
		textSize = defaultSize;
		this.printer = printer;

		if (family != null) {
			familyName = new String(family);
		}

		if (fixedPitch) {
			pitch = IFIMetrics.PITCH_VARIABLE;
		} else {
			pitch = IFIMetrics.PITCH_VARIABLE;
		}
		this.charSet = charSet;
		this.weight = weight;

		selection = IFIMetrics.FM_SEL_REGULAR;
		if (style == Typeface.BOLD) {
			selection = IFIMetrics.FM_SEL_BOLD;
		} else if (style == Typeface.ITALIC) {
			selection = IFIMetrics.FM_SEL_ITALIC;
		} else if (style == Typeface.BOLD_ITALIC) {
			selection = IFIMetrics.FM_SEL_BOLD + IFIMetrics.FM_SEL_ITALIC;
		}
		if (script) {
			fontFamily = IFIMetrics.FF_SCRIPT;
		} else {
			fontFamily = IFIMetrics.FF_DONTCARE;
		}
	}

	/**
	 * Set face name and convert the name to a general name. e.g. from
	 * Arial,Bold to Arial-Bold So the font will be substituted with a closer
	 * font.
	 * 
	 * @param faceName
	 *            the face name to use
	 */
	private void setFaceName(String faceName) {
		if (faceName != null) {
			this.faceName = new String(faceName);
			// convert PDF specific name to general name
			// e.g. Arial,Bold --> Arial-Bold
			if (this.faceName.endsWith(",Bold") || this.faceName.endsWith(",Italic")
					|| this.faceName.endsWith(",BoldItalic")) {
				int index = this.faceName.lastIndexOf(',');
				String face = this.faceName.substring(0, index);
				String style = this.faceName.substring(index + 1);
				this.faceName = new String(face + "-" + style);
			}
		}
	}

	/**
	 * Constructor for embedded fonts
	 * 
	 * @param printer
	 *            printer object
	 * @param faceName
	 *            face name which is used even if a different name exist in the
	 *            font stream
	 * @param fontRef
	 *            unique id for this font
	 * @param reader
	 *            logical stream reader for embedded font
	 * @param fontStreamType
	 *            the type of fontSteam. This must be 1, 2 or 3.
	 * @param subtype
	 *            sub-type of this font
	 * @param charNameStrings
	 *            character name table used in the font stream. This cannot be
	 *            null for fontSteamType 1 and 3.
	 */
	public OFont(Printer printer, String faceName, ILogicalStreamReader reader,
			int[] fontStreamLengths, int fontStreamType, String subtype, Vector<?> charNameStrings) {
		if (printer == null) {
			throw new NullPointerException();
		}
		setFaceName(faceName);
		textSize = defaultSize;
		this.printer = printer;

		byte fontType = 0;
		byte encodingType = 0;
		switch (fontStreamType) {
		case 1:
			fontType = FontType.POSTSCRIPT;
			encodingType = EncodingType.CHARSET;
			break;
		case 2:
			fontType = FontType.TRUETYPE;
			encodingType = EncodingType.GLYPHINDEX;
			break;
		case 3:
			fontType = FontType.POSTSCRIPT;
			if (subtype.equals("Type1")) {
				encodingType = EncodingType.CHARSET;
			} else { // CIDFontType2
				encodingType = EncodingType.GLYPHINDEX;
			}
			break;
		default:
			throw new UnsupportedOperationException();
		}
		fontObj = new EmbeddedFontData(reader, fontStreamLengths, fontType, encodingType,
				charNameStrings);
	}

	/**
	 * Constructor for Type 3 fonts
	 * 
	 * @param printer
	 *            printer object
	 * @param faceName
	 *            face name which is used even if a different name exist in the
	 *            font stream
	 * @param fontBBox
	 *            bounding box of the font.
	 * @param glyphShapes
	 *            drawing instructions of all the fonts in the font
	 * @param charNameStrings
	 *            character name table used in the font stream. This cannot be
	 *            null for fontSteamType 1 and 3.
	 */
	public OFont(Printer printer, String faceName, PRectangle fontBBox, HashMap<?, ?> glyphShapes,
			Vector<?> charNameStrings) {
		if (printer == null) {
			throw new NullPointerException();
		}
		setFaceName(faceName);
		textSize = defaultSize;
		this.printer = printer;
		this.fontBBox = fontBBox;
		fontObj = new Type3Font(faceName, glyphShapes);
		Type3Font type3Font = (Type3Font) fontObj;
		type3Font.setCharacterNames(charNameStrings);
	}

	private OFont(OFont font) {
		if (font == null) {
			throw new NullPointerException();
		}
		setFaceName(font.faceName);
		this.printer = font.printer;

		if (font.familyName != null) {
			this.familyName = new String(font.familyName);
		}
		this.textSize = font.textSize;
		this.toUnicode = font.toUnicode;
		this.missingWidth = font.missingWidth;
		this.firstCh = font.firstCh;
		this.ascent = font.ascent;
		this.descent = font.descent;
		this.widths = font.widths;
		this.cidWidths = font.cidWidths;
		this.cidDefaultWidth = font.cidDefaultWidth;
		this.cidVMetrics = font.cidVMetrics;
		this.cidDefaultVMetrics = font.cidDefaultVMetrics;
		this.cMap = font.cMap;
		if (font.transform != null) {
			this.transform = new AffineTransform(font.transform);
		}
		this.printer = font.printer;
		this.pitch = font.pitch;
		this.charSet = font.charSet;
		this.selection = font.selection;
		this.fontFamily = font.fontFamily;
		this.weight = font.weight;
		this.fontObj = font.fontObj;
		this.fontBBox = font.fontBBox;
		this.CIDToGIDMap = font.CIDToGIDMap;
		this.toCID = font.toCID;
		this.CIDToUnicode = font.CIDToUnicode;
		this.toGIDMap = font.toGIDMap;
	}

	/**
	 * Generate unicode table from glyph names talbe
	 * 
	 * @param glyphNames
	 *            glyph Names table, which is referred to by char code. The size
	 *            of table must not be over 256.
	 * @return unicode table being referred to by char code
	 */
	private int[] getUnicodeTable(Vector<?> glyphNames) {
		int[] unicodeTable = new int[256];

		char[] ch = new char[1];
		String glyphName;
		int index;
		int sizeOfCharNames = glyphNames.size();
		for (index = 0; index < 256; index++) {
			glyphName = null;
			if (index < sizeOfCharNames) {
				glyphName = (String) glyphNames.elementAt(index);
			}

			if (glyphName != null) {
				ch[0] = (char) (index);
				String str = toUnicode(new String(ch));
				unicodeTable[index] = ((int) str.charAt(0) & 0xffff);
			} else {
				unicodeTable[index] = -1;
			}
		}

		return unicodeTable;
	}

	/**
	 * call JPrintEngine to realise the font
	 * 
	 * @param g
	 * @throws PDFPrintException
	 */
	private void realizeFont(Canvas g) throws PDFPrintException {
		try {
			com.softwareimaging.print.Printer jpePrinter = printer.getJPrintEnginePrinter();
			if (fontObj != null) {
				if (fontObj instanceof EmbeddedFontData) {
					EmbeddedFontData fd = (EmbeddedFontData) fontObj;

					short flags = 0;
					iFont = jpePrinter.createFont(faceName, fd.reader, fd.fontStreamLengths,
							fd.fontType, fd.encodingType, flags, fd.cs);

				} else if (fontObj instanceof Type3Font) {
					if (g == null) {
						// We can only realize a t3 font with a canvas
						throw new UnsupportedOperationException();
					}
					Type3Font t3Font = (Type3Font) fontObj;

					iFont = jpePrinter.createFont(t3Font.getFontName(), g.toMatrix(transform),
							fontBBox.toJPECoordinates(), getCharProcs(g), t3Font
									.getCharacterNames());
				}
			}

			if (iFont == null) {
				iFont = jpePrinter.createFont(faceName, familyName, charSet, pitch, fontFamily,
						selection, 0, weight, textSize);
			} else {
				iFont.setPointSize(this.textSize);
			}
		} catch (ArgumentRangeException e) {
			Logger.outputStackTrace(e);
		}
	}

	/**
	 * Create ToGIDMap
	 */
	private void createToGIDMap() {
		EmbeddedFontData fd = (EmbeddedFontData)fontObj;
		Vector gn = fd.cs.glyphNames;
		toGIDMap = new char[gn.size()];

		try {
			TTFontParser ttParser = new TTFontParser(fd.reader);
			TTcmap.Format format = new TTcmap(ttParser).getFormat();
			java.util.HashMap charStrings = format.createCharStrings(fd.cs);

			for (int index = 0; index < gn.size(); index++) {
				String string = (String) gn.elementAt(index);
				toGIDMap[index] = 0;
				if (string != null) {
					Integer gid = (Integer)charStrings.get(string);
					if (gid != null) {
						toGIDMap[index] = (char)gid.intValue();
					}
				}
			}
		} catch (TTFontParserException e) {
			Logger.outputStackTrace(e);
		}
	}

	public FontFile deriveFont(CMap toUnicode, char[] diff, CMap toCID, CMap CIDToUnicode,
			byte[] CIDToGIDMap) {
		OFont font = new OFont(this);
		font.toUnicode = toUnicode;
		font.cMap = diff;
		font.CIDToGIDMap = CIDToGIDMap;
		font.toCID = toCID;
		font.CIDToUnicode = CIDToUnicode;

		if (fontObj != null && fontObj instanceof EmbeddedFontData) {
			EmbeddedFontData fd = new EmbeddedFontData((EmbeddedFontData) fontObj);
			font.fontObj = fd;
			if (fd.cs.glyphNames != null && fd.fontType == FontType.TRUETYPE) {
				fd.cs.toUnicode = font.getUnicodeTable(fd.cs.glyphNames);
				font.createToGIDMap();
			}
		}

		return font;
	}

	public FontFile deriveFont(float[] widths, int firstCh, float missingWidth, float ascent,
			float descent) {
		OFont font = new OFont(this);
		font.firstCh = firstCh;
		font.widths = widths;
		font.missingWidth = missingWidth;
		font.ascent = ascent;
		font.descent = descent;
		return font;
	}

	public FontFile deriveFont(Map<Integer, Float> widths, Float cidDefaultWidth,
			Map<Integer, CIDVerticalMetrics> cidVMetrics, CIDVerticalMetrics cidDefaultVMetrics,
			float missingWidth, float ascent, float descent) {
		OFont font = new OFont(this);
		font.cidDefaultWidth = cidDefaultWidth;
		font.missingWidth = missingWidth;
		font.ascent = ascent;
		font.descent = descent;
		font.cidWidths = widths;
		font.cidVMetrics = cidVMetrics;
		font.cidDefaultVMetrics = cidDefaultVMetrics;
		return font;
	}

	public FontFile deriveFont(AffineTransform at) {
		OFont font = new OFont(this);
		if (at == null) {
			font.transform = null;
		} else {
			font.transform = new AffineTransform(at);
		}
		return font;
	}

	public boolean canDisplayEchar(char ech) {
		return true;
	}

	public FontFile deriveFont(float pointsize) {
		OFont font = new OFont(this);
		font.textSize = pointsize;
		return font;
	}

	/**
	 * Returns character advance in text space.
	 * 
	 * @param ech
	 *            character to measure
	 * @return character advance
	 * @throws PDFPrintException
	 */
	public PointF echarAdvance(final char ech) throws PDFPrintException {
		float advance = Float.MIN_VALUE;
		float advanceY = 0.0f;
		char cid = 0;
		final float textSize = getSize(transform);

		if (cidWidths != null || cidVMetrics != null) {
			cid = (toCID != null ? (char) toCID.toSelector(ech) : ech);
		}

		if (widths != null) {
			if (ech - firstCh >= 0 && ech - firstCh < widths.length) {
				advance = widths[ech - firstCh] * textSize;
			} else if (missingWidth > 0) {
				advance = missingWidth * textSize;
			}
		}

		Float cidWidth = null;
		if (cidWidths != null) {
			cidWidth = cidWidths.get((int) cid);
		}
		if (cidWidth != null) {
			advance = cidWidth * textSize;
		} else if (cidDefaultWidth != null) {
			advance = cidDefaultWidth * textSize;
		}

		// get vertical font information
		CIDVerticalMetrics vMetrics = null;
		if (cidVMetrics != null) {
			vMetrics = cidVMetrics.get((int) cid);
		}
		if (vMetrics != null) {
			advanceY = vMetrics.advance * textSize;
		} else if (cidDefaultVMetrics != null) {
			advanceY = cidDefaultVMetrics.advance * textSize;
		}

		if (advance == Float.MIN_VALUE) {
			// check cache
			String text = String.valueOf(ech);
			PointF echarAdvance = echarAdvanceCache.get(text);

			// generate metrics is needed
			if (echarAdvance == null) {
				advance = getDeviceCharWidth(ech);
				advanceY = 0.0f;
				echarAdvanceCache.put(text, new PointF(advance, advanceY));
			}
			// returned cashed value
			else {
				advance = echarAdvance.x;
				advanceY = echarAdvance.y;
			}
		}

		return new PointF(advance, advanceY);
	}

	public PointF echarAdvance(final char ech, boolean fromFont) throws PDFPrintException {
		if (fromFont && fontObj == null) {
			float advance = getDeviceCharWidth(ech);
			float advanceY = 0.0f;
			return new PointF(advance, advanceY);
		}
		return echarAdvance(ech);
	}

	/**
	 * Gets the ToUnicode character value for the given character.
	 * 
	 * @param currentChar
	 *            character to find a corresponding CMap for.
	 * @return a new Character based on the CMap transformation. If the
	 *         character can not be found in the CMap the original value is
	 *         returned.
	 */
	private int getCMapping(char currentChar) {
		int ch = (int) currentChar & 0xffff;
		if (toUnicode != null) {
			ch = toUnicode.toSelector(currentChar);
		} else if (CIDToUnicode != null) {
			if (toCID != null) {
				ch = CIDToUnicode.toSelector((char) toCID.toSelector(currentChar));
			} else {
				// Identity mapping
				ch = CIDToUnicode.toSelector(currentChar);
			}
		}
		return ch;
	}

	/**
	 * Return the width of the given character
	 * 
	 * @param character
	 *            character to retreive width of
	 * @return width of the given <code>character</code>
	 */
	private char getCharDiff(char character) {
		if (cMap != null && character < cMap.length) {
			return cMap[character];
		} else {
			return character;
		}
	}

	/**
	 * Returns text size in text space using default font transform.
	 * 
	 * @return text size in text space
	 */
	public float getSize() {
		return getSize(null);
	}

	/**
	 * Returns text size in text space using transform parameter.
	 * 
	 * @return text size in text space
	 */
	private float getSize(AffineTransform transform) {
		if (transform != null) {
			// "FontMatrix" is described as a 1000-unit glyph coordinate system.
			float pts[] = new float[] { textSize * 1000, 0 };
			transform.mapPoints(pts);
			return pts[0];
		}
		return textSize;
	}

	public AffineTransform getTransform() {
		return transform;
	}

	public String getName() {
		return faceName;
	}

	/**
	 * Returns whether text is sent as Unicode or not
	 * 
	 * @return true if Unicode output is required
	 */
	private boolean IsUnicodeText() {
		// Characters for embedded and Type3 fonts are indeces to a table in the
		// fonts, not Unicode.
		if (fontObj != null) {
			return false;
		}

		// Symbol has no Unicode-charcode map. So, characters are usually sent
		// as glyph indeces.
		if (charSet == IFIMetrics.CS_SYMBOL) {
			return false;
		}

		return true; // text should be sent as Unicode
	}

	private Vector<CharProc> getCharProcs(Canvas g) throws PDFPrintException {
		Vector<CharProc> procs = new Vector<CharProc>();

		Type3Font type3Font = (Type3Font) fontObj;
		HashMap<?, ?> charShapes = type3Font.getCharShapes();
		if (charShapes != null) {
			Iterator<?> iterator = charShapes.keySet().iterator();

			String charName = null;
			Shapes shapes = null;
			PRectangle bbox = null;
			PointF displacement = null;
			RectangleF boundBox = null;

			while (iterator.hasNext()) {
				charName = (String) iterator.next();
				shapes = (Shapes) charShapes.get(charName);
				bbox = shapes.getType3BBox();
				displacement = shapes.getType3HorizontalDisplacement();
				boundBox = null;
				if (bbox != null) {
					boundBox = bbox.toJPECoordinates();
				}

				g.startRecordingOperations();
				g.clipRect((RectF) null, Op.REPLACE);

				// Set text space.
				g.setTransform(new AffineTransform());
				g.scale(1, -1);

				shapes.paint(g);

				g.endRecordingOperations();
				procs.add(new CharProc(charName, new SizeF(displacement.x, displacement.y),
						boundBox, g.getOperations()));
				g.clearOperations();
			}
			charShapes.clear();
			type3Font.setCharShapes(null);
		}
		return procs;
	}

	private float getDeviceCharWidth(char ech) throws PDFPrintException {
		if (printer == null) {
			throw new UnsupportedOperationException();
		}

		if (iFont == null) {
			realizeFont(null);
		}

		float width;
		String s = String.valueOf(ech);

		if (IsUnicodeText()) {
			width = iFont.charWidth(toUnicode(s).charAt(0));
		} else {
			width = iFont.charWidth(toGlyphIndex(s).charAt(0));
		}
		return width;
	}

	public void drawEstring(Canvas g, Paint paint, String displayText, float x, float y, long layout, int mode,
			Color strokecolor) throws PDFPrintException {
		if (iFont == null) {
			realizeFont(g);
		}

		paint.setColor(strokecolor.getRGB());
		if (IsUnicodeText()) {
			g.drawText(toUnicode(displayText), x, y, paint, iFont, mode, strokecolor);
		} else {
			g.drawText(toGlyphIndex(displayText), x, y, paint, iFont, mode, strokecolor);
		}
	}

	/*
	 * Check if a character exists in the code range
	 * 
	 * @param ch character to be checked
	 * 
	 * @param cb length of character in byte
	 * 
	 * @return true if the character exists
	 */
	public boolean inCodeSpaceRange(char ch, byte length) {
		if (toCID != null) {
			return toCID.inCodeSpaceRange(ch, length);
		}
		// If toCID is null, that means Identity-H/-V CMap is used and the size
		// of characters is always two-byte.
		return length == 2;
	}

	private String toCID(String text) {
		if (toCID != null) {
			StringBuffer sb = new StringBuffer(text.length());
			char ch;
			for (int i = 0; i < text.length(); i++) {
				ch = text.charAt(i);
				ch = (char) toCID.toSelector(ch);
				sb.append(ch);
			}
			return sb.toString();
		}
		return text;
	}

	private String toGlyphIndex(String text) {
		if (toGIDMap != null) {
			// This case is for embedded TrueType with EncodingType.CHARSET 
			StringBuffer sb = new StringBuffer(text.length());
			for (int i = 0; i < text.length(); i++) {
				sb.append(toGIDMap[text.charAt(i)]);
			}
			return sb.toString();
		} else {
			text = toCID(text);
			if (CIDToGIDMap != null) {
				StringBuffer sb = new StringBuffer(text.length());
				char cid, gid;
				for (int i = 0; i < text.length(); i++) {
					cid = text.charAt(i);
					gid = (char) (CIDToGIDMap[2 * cid] << 8);
					gid |= (CIDToGIDMap[2 * cid + 1] & 0x00ff);
					sb.append(gid);
				}
				return sb.toString();
			}
		}
		return text;
	}

	public String toUnicode(String text) {
		StringBuffer sb = new StringBuffer(text.length());
		for (int i = 0; i < text.length(); i++) {
			// get the first char in the buffer
			char c1 = text.charAt(i);

			// the toUnicode map is used for font substitution and especially
			// for CID fonts. If toUnicode is available
			// we use it as is, if not then we can use the charDiff mapping,
			// which takes care of font encoding differences.
			char c = (toUnicode == null && CIDToUnicode == null) ? getCharDiff(c1) : c1;

			// check for CMap ToUnicode properties.
			int ch = getCMapping(c);

			if ((ch & 0xffff0000) != 0) {
				sb.append((char) (ch >> 16)); // High surrogate
			}
			c = (char) ch;
			sb.append(c);
		}
		return sb.toString();
	}
}
