PdfType0Font.java

/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2025 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package com.itextpdf.kernel.font;

import com.itextpdf.commons.exceptions.ITextException;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.CFFFontSubset;
import com.itextpdf.io.font.CMapEncoding;
import com.itextpdf.io.font.CidFont;
import com.itextpdf.io.font.CidFontProperties;
import com.itextpdf.io.font.FontProgram;
import com.itextpdf.io.font.FontProgramFactory;
import com.itextpdf.io.font.PdfEncodings;
import com.itextpdf.io.font.TrueTypeFont;
import com.itextpdf.io.font.cmap.CMapCharsetEncoder;
import com.itextpdf.io.font.cmap.CMapContentParser;
import com.itextpdf.io.font.cmap.CMapToUnicode;
import com.itextpdf.io.font.cmap.StandardCMapCharsets;
import com.itextpdf.io.font.otf.Glyph;
import com.itextpdf.io.font.otf.GlyphLine;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.source.ByteArrayOutputStream;
import com.itextpdf.io.source.ByteBuffer;
import com.itextpdf.io.source.HighPrecisionOutputStream;
import com.itextpdf.io.util.StreamUtil;
import com.itextpdf.io.util.TextUtil;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfArray;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfLiteral;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfOutputStream;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfVersion;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PdfType0Font extends PdfFont {

    /**
     * This is the default encoding to use.
     */
    private static final String DEFAULT_ENCODING = "";

    /**
     * The code length shall not be greater than 4.
     */
    private static final int MAX_CID_CODE_LENGTH = 4;
    private static final byte[] rotbits = {(byte) 0x80, (byte) 0x40, (byte) 0x20, (byte) 0x10, (byte) 0x08, (byte) 0x04, (byte) 0x02, (byte) 0x01};

    /**
     * CIDFont Type0 (Type1 outlines).
     */
    protected static final int CID_FONT_TYPE_0 = 0;
    /**
     * CIDFont Type2 (TrueType outlines).
     */
    protected static final int CID_FONT_TYPE_2 = 2;

    protected boolean vertical;
    protected CMapEncoding cmapEncoding;
    protected Set<Integer> usedGlyphs;
    protected int cidFontType;
    protected char[] specificUnicodeDifferences;

    private final CMapToUnicode embeddedToUnicode;

    PdfType0Font(TrueTypeFont ttf, String cmap) {
        super();
        if (!PdfEncodings.IDENTITY_H.equals(cmap) && !PdfEncodings.IDENTITY_V.equals(cmap)) {
            throw new PdfException(KernelExceptionMessageConstant.ONLY_IDENTITY_CMAPS_SUPPORTS_WITH_TRUETYPE);
        }

        if (!ttf.getFontNames().allowEmbedding()) {
            throw new PdfException(KernelExceptionMessageConstant.CANNOT_BE_EMBEDDED_DUE_TO_LICENSING_RESTRICTIONS)
                    .setMessageParams(ttf.getFontNames().getFontName() + ttf.getFontNames().getStyle());
        }
        this.fontProgram = ttf;
        this.embedded = true;
        vertical = cmap.endsWith("V");
        cmapEncoding = new CMapEncoding(cmap);
        usedGlyphs = new TreeSet<>();
        cidFontType = CID_FONT_TYPE_2;
        embeddedToUnicode = null;
        if (ttf.isFontSpecific()) {
            specificUnicodeDifferences = new char[256];
            byte[] bytes = new byte[1];
            for (int k = 0; k < 256; ++k) {
                bytes[0] = (byte) k;
                String s = PdfEncodings.convertToString(bytes, null);
                char ch = s.length() > 0 ? s.charAt(0) : '?';
                specificUnicodeDifferences[k] = ch;
            }
        }
    }

    // Note. Make this constructor protected. Only PdfFontFactory (kernel level) will
    // be able to create Type0 font based on predefined font.
    // Or not? Possible it will be convenient construct PdfType0Font based on custom CidFont.
    // There is no typography features in CJK fonts.
    PdfType0Font(CidFont font, String cmap) {
        super();
        if (!CidFontProperties.isCidFont(font.getFontNames().getFontName(), cmap)) {
            throw new PdfException("Font {0} with {1} encoding is not a cjk font.")
                    .setMessageParams(font.getFontNames().getFontName(), cmap);
        }
        this.fontProgram = font;
        vertical = cmap.endsWith("V");
        String uniMap = getCompatibleUniMap(fontProgram.getRegistry());
        cmapEncoding = new CMapEncoding(cmap, uniMap);
        usedGlyphs = new TreeSet<>();
        cidFontType = CID_FONT_TYPE_0;
        embeddedToUnicode = null;
    }

    PdfType0Font(PdfDictionary fontDictionary) {
        super(fontDictionary);
        newFont = false;
        PdfDictionary cidFont = fontDictionary.getAsArray(PdfName.DescendantFonts).getAsDictionary(0);
        PdfObject cmap = fontDictionary.get(PdfName.Encoding);

        String ordering = getOrdering(cidFont);
        if(ordering == null) {
            throw new PdfException(KernelExceptionMessageConstant.ORDERING_SHOULD_BE_DETERMINED);
        }
        CMapToUnicode toUnicodeCMap;
        PdfObject toUnicode = fontDictionary.get(PdfName.ToUnicode);
        if (toUnicode == null) {
            toUnicodeCMap = FontUtil.parseUniversalToUnicodeCMap(ordering);
            embeddedToUnicode = null;
        } else {
            toUnicodeCMap = FontUtil.processToUnicode(toUnicode);
            embeddedToUnicode = toUnicodeCMap;
        }

        if (cmap.isName() && ((toUnicodeCMap != null) || PdfEncodings.IDENTITY_H.equals(((PdfName) cmap).getValue()) ||
                PdfEncodings.IDENTITY_V.equals(((PdfName) cmap).getValue()))) {

            if (toUnicodeCMap == null) {
                String uniMap = getUniMapFromOrdering(ordering, PdfEncodings.IDENTITY_H.equals(((PdfName) cmap).getValue()));
                toUnicodeCMap = FontUtil.getToUnicodeFromUniMap(uniMap);
                if (toUnicodeCMap == null) {
                    toUnicodeCMap = FontUtil.getToUnicodeFromUniMap(PdfEncodings.IDENTITY_H);
                    Logger logger = LoggerFactory.getLogger(PdfType0Font.class);
                    logger.error(MessageFormatUtil.format(IoLogMessageConstant.UNKNOWN_CMAP, uniMap));
                }
            }
            fontProgram = DocTrueTypeFont.createFontProgram(cidFont, toUnicodeCMap);
            cmapEncoding = createCMap(cmap, null);
            assert fontProgram instanceof IDocFontProgram;
            embedded = ((IDocFontProgram) fontProgram).getFontFile() != null;
        } else {
            String cidFontName = cidFont.getAsName(PdfName.BaseFont).getValue();
            String uniMap = getUniMapFromOrdering(ordering, true);
            if (uniMap != null && uniMap.startsWith("Uni") && CidFontProperties.isCidFont(cidFontName, uniMap)) {
                try {
                    fontProgram = FontProgramFactory.createFont(cidFontName);
                    cmapEncoding = createCMap(cmap, uniMap);
                    embedded = false;
                } catch (IOException ignored) {
                    fontProgram = null;
                    cmapEncoding = null;
                }
            } else {
                if (toUnicodeCMap == null) {
                    toUnicodeCMap = FontUtil.getToUnicodeFromUniMap(uniMap);
                }
                if (toUnicodeCMap != null) {
                    fontProgram = DocTrueTypeFont.createFontProgram(cidFont, toUnicodeCMap);
                    cmapEncoding = createCMap(cmap, uniMap);
                }
            }
            if (fontProgram == null) {
                throw new PdfException(MessageFormatUtil.format(
                        KernelExceptionMessageConstant.CANNOT_RECOGNISE_DOCUMENT_FONT_WITH_ENCODING,
                        cidFontName, cmap));
            }
        }
        // DescendantFonts is a one-element array specifying the CIDFont dictionary
        // that is the descendant of this Type 0 font.
        PdfDictionary cidFontDictionary = fontDictionary.getAsArray(PdfName.DescendantFonts).getAsDictionary(0);
        // Required according to the spec
        PdfName subtype = cidFontDictionary.getAsName(PdfName.Subtype);
        if (PdfName.CIDFontType0.equals(subtype)) {
            cidFontType = CID_FONT_TYPE_0;
        } else if (PdfName.CIDFontType2.equals(subtype)) {
            cidFontType = CID_FONT_TYPE_2;
        } else {
            LoggerFactory.getLogger(getClass()).error(IoLogMessageConstant.FAILED_TO_DETERMINE_CID_FONT_SUBTYPE);
        }
        usedGlyphs = new TreeSet<>();
        subset = false;
    }

    /**
     * Get Unicode mapping name from ordering.
     * @param ordering the text ordering to base to unicode mapping on
     * @param horizontal identifies whether the encoding is horizontal or vertical
     *
     * @return Unicode mapping name
     */
    public static String getUniMapFromOrdering(String ordering, boolean horizontal) {
        String result = null;
        switch (ordering) {
            case "CNS1":
                result = "UniCNS-UTF16-";
                break;
            case "Japan1":
                result = "UniJIS-UTF16-";
                break;
            case "Korea1":
                result = "UniKS-UTF16-";
                break;
            case "GB1":
                result = "UniGB-UTF16-";
                break;
            case "Identity":
                result = "Identity-";
                break;
            default:
                return null;
        }
        if (horizontal) {
            return result + 'H';
        }
        return result + 'V';
    }

    @Override
    public Glyph getGlyph(int unicode) {
        // TODO DEVSIX-7568 handle unicode value with cmap and use only glyphByCode
        Glyph glyph = getFontProgram().getGlyph(unicode);
        if (glyph == null && (glyph = notdefGlyphs.get(unicode)) == null) {
            // Handle special layout characters like softhyphen (00AD).
            // This glyphs will be skipped while converting to bytes
            Glyph notdef = getFontProgram().getGlyphByCode(0);
            if (notdef != null) {
                glyph = new Glyph(notdef, unicode);
            } else {
                glyph = new Glyph(-1, 0, unicode);
            }
            notdefGlyphs.put(unicode, glyph);
        }
        return glyph;
    }

    @Override
    public boolean containsGlyph(int unicode) {
        if (cidFontType == CID_FONT_TYPE_0) {
            if (cmapEncoding.isDirect()) {
                return fontProgram.getGlyphByCode(unicode) != null;
            } else {
                return getFontProgram().getGlyph(unicode) != null;
            }
        } else if (cidFontType == CID_FONT_TYPE_2) {
            if (fontProgram.isFontSpecific()) {
                byte[] b = PdfEncodings.convertToBytes((char) unicode, "symboltt");
                return b.length > 0 && fontProgram.getGlyph(b[0] & 0xff) != null;
            } else {
                return getFontProgram().getGlyph(unicode) != null;
            }
        } else {
            throw new PdfException("Invalid CID font type: " + cidFontType);
        }
    }

    private byte[] convertToBytesUsingCMap(String text) {
        int len = text.length();
        ByteBuffer buffer = new ByteBuffer();
        if (fontProgram.isFontSpecific()) {
            byte[] b = PdfEncodings.convertToBytes(text, "symboltt");
            len = b.length;
            for (int k = 0; k < len; ++k) {
                Glyph glyph = fontProgram.getGlyph(b[k] & 0xff);
                if (glyph != null) {
                    convertToBytes(glyph, buffer);
                }
            }
        } else {
            for (int k = 0; k < len; ++k) {
                int val;
                if (TextUtil.isSurrogatePair(text, k)) {
                    val = TextUtil.convertToUtf32(text, k);
                    k++;
                } else {
                    val = text.charAt(k);
                }
                Glyph glyph = getGlyph(val);
                if (glyph.getCode() > 0) {
                    convertToBytes(glyph, buffer);
                } else {
                    //getCode() could be either -1 or 0
                    buffer.append(cmapEncoding.getCmapBytes(0));
                }
            }
        }
        return buffer.toByteArray();
    }

    @Override
    public byte[] convertToBytes(String text) {
        CMapCharsetEncoder encoder = StandardCMapCharsets.getEncoder(cmapEncoding.getCmapName());
        if (encoder == null) {
            return this.convertToBytesUsingCMap(text);
        } else {
            return converToBytesUsingEncoder(text, encoder);
        }
    }

    private byte[] converToBytesUsingEncoder(String text, CMapCharsetEncoder encoder) {
        java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
        int[] codePoints = TextUtil.convertToUtf32(text);
        for (int cp : codePoints) {
            try {
                stream.write(encoder.encodeUnicodeCodePoint(cp));
                Glyph glyph = getGlyph(cp);
                if (glyph.getCode() > 0) {
                    usedGlyphs.add(glyph.getCode());
                }
            } catch (IOException e) {
                // can only be thrown when stream is closed
                throw new ITextException(e);
            }
        }
        return stream.toByteArray();
    }

    @Override
    public byte[] convertToBytes(GlyphLine glyphLine) {
        if (glyphLine == null) {
            return new byte[0];
        }
        // NOTE: this isn't particularly efficient, but it demonstrates the principle behind CMap-less conversion
        // (i.e. we only use the CMap's name to derive the correct encoding)
        // Also, it will yield wrong results when used in an embedded setting where font features have been applied
        CMapCharsetEncoder encoder = StandardCMapCharsets.getEncoder(cmapEncoding.getCmapName());
        if (encoder == null) {
            int totalByteCount = 0;
            for (int i = glyphLine.getStart(); i < glyphLine.getEnd(); i++) {
                totalByteCount += cmapEncoding.getCmapBytesLength(glyphLine.get(i).getCode());
            }
            // perform actual conversion
            byte[] bytes = new byte[totalByteCount];
            int offset = 0;
            for (int i = glyphLine.getStart(); i < glyphLine.getEnd(); i++) {
                usedGlyphs.add(glyphLine.get(i).getCode());
                offset = cmapEncoding.fillCmapBytes(glyphLine.get(i).getCode(), bytes, offset);
            }
            return bytes;
        } else {
            java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
            for (int i = glyphLine.getStart(); i < glyphLine.getEnd(); i++) {
                Glyph g = glyphLine.get(i);
                usedGlyphs.add(g.getCode());
                byte[] encodedBit = encoder.encodeUnicodeCodePoint(g.getUnicode());
                try {
                    baos.write(encodedBit);
                } catch (IOException e) {
                    // could only be thrown when the stream is closed
                    throw new PdfException(e);
                }
            }
            return baos.toByteArray();
        }
    }

    @Override
    public byte[] convertToBytes(Glyph glyph) {
        usedGlyphs.add(glyph.getCode());
        CMapCharsetEncoder encoder = StandardCMapCharsets.getEncoder(cmapEncoding.getCmapName());
        if (encoder == null) {
            return cmapEncoding.getCmapBytes(glyph.getCode());
        } else {
            int cp = glyph.getUnicode();
            return encoder.encodeUnicodeCodePoint(cp);
        }
    }

    @Override
    public void writeText(GlyphLine text, int from, int to, PdfOutputStream stream) {
        int len = to - from + 1;
        if (len > 0) {
            byte[] bytes = convertToBytes(new GlyphLine(text, from, to + 1));
            StreamUtil.writeHexedString(stream, bytes);
        }
    }

    @Override
    public void writeText(String text, PdfOutputStream stream) {
        StreamUtil.writeHexedString(stream, convertToBytes(text));
    }

    @Override
    public GlyphLine createGlyphLine(String content) {
        List<Glyph> glyphs = new ArrayList<>();
        if (cidFontType == CID_FONT_TYPE_0) {
            int len = content.length();
            if (cmapEncoding.isDirect()) {
                for (int k = 0; k < len; ++k) {
                    Glyph glyph = fontProgram.getGlyphByCode((int) content.charAt(k));
                    if (glyph != null) {
                        glyphs.add(glyph);
                    }
                }
            } else {
                for (int k = 0; k < len; ++k) {
                    int ch;
                    if (TextUtil.isSurrogatePair(content, k)) {
                        ch = TextUtil.convertToUtf32(content, k);
                        k++;
                    } else {
                        ch = content.charAt(k);
                    }
                    glyphs.add(getGlyph(ch));
                }
            }
        } else if (cidFontType == CID_FONT_TYPE_2) {
            int len = content.length();
            if (fontProgram.isFontSpecific()) {
                byte[] b = PdfEncodings.convertToBytes(content, "symboltt");
                len = b.length;
                for (int k = 0; k < len; ++k) {
                    Glyph glyph = fontProgram.getGlyph(b[k] & 0xff);
                    if (glyph != null) {
                        glyphs.add(glyph);
                    }
                }
            } else {
                for (int k = 0; k < len; ++k) {
                    int val;
                    if (TextUtil.isSurrogatePair(content, k)) {
                        val = TextUtil.convertToUtf32(content, k);
                        k++;
                    } else {
                        val = content.charAt(k);
                    }
                    glyphs.add(getGlyph(val));
                }
            }
        } else {
            throw new PdfException("Font has no suitable cmap.");
        }

        return new GlyphLine(glyphs);
    }

    @Override
    public int appendGlyphs(String text, int from, int to, List<Glyph> glyphs) {
        if (cidFontType == CID_FONT_TYPE_0) {
            if (cmapEncoding.isDirect()) {
                int processed = 0;
                for (int k = from; k <= to; k++) {
                    Glyph glyph = fontProgram.getGlyphByCode((int) text.charAt(k));
                    if (glyph != null && (isAppendableGlyph(glyph))) {
                        glyphs.add(glyph);
                        processed++;
                    } else {
                        break;
                    }
                }
                return processed;
            } else {
                return appendUniGlyphs(text, from, to, glyphs);
            }
        } else if (cidFontType == CID_FONT_TYPE_2) {
            if (fontProgram.isFontSpecific()) {
                int processed = 0;
                for (int k = from; k <= to; k++) {
                    Glyph glyph = fontProgram.getGlyph(text.charAt(k) & 0xff);
                    if (glyph != null && (isAppendableGlyph(glyph))) {
                        glyphs.add(glyph);
                        processed++;
                    } else {
                        break;
                    }
                }
                return processed;
            } else {
                return appendUniGlyphs(text, from, to, glyphs);
            }
        } else {
            throw new PdfException("Font has no suitable cmap.");
        }
    }

    private int appendUniGlyphs(String text, int from, int to, List<Glyph> glyphs) {
        int processed = 0;
        for (int k = from; k <= to; ++k) {
            int val;
            int currentlyProcessed = processed;
            if (TextUtil.isSurrogatePair(text, k)) {
                val = TextUtil.convertToUtf32(text, k);
                processed += 2;
                // Since a pair is processed, need to skip next char as well
                k += 1;
            } else {
                val = text.charAt(k);
                processed++;
            }
            Glyph glyph = getGlyph(val);
            if (isAppendableGlyph(glyph)) {
                glyphs.add(glyph);
            } else {
                processed = currentlyProcessed;
                break;
            }
        }
        return processed;
    }

    @Override
    public int appendAnyGlyph(String text, int from, List<Glyph> glyphs) {
        int process = 1;

        if (cidFontType == CID_FONT_TYPE_0) {
            if (cmapEncoding.isDirect()) {
                Glyph glyph = fontProgram.getGlyphByCode((int) text.charAt(from));
                if (glyph != null) {
                    glyphs.add(glyph);
                }
            } else {
                int ch;
                if (TextUtil.isSurrogatePair(text, from)) {
                    ch = TextUtil.convertToUtf32(text, from);
                    process = 2;
                } else {
                    ch = text.charAt(from);
                }
                glyphs.add(getGlyph(ch));
            }
        } else if (cidFontType == CID_FONT_TYPE_2) {
            TrueTypeFont ttf = (TrueTypeFont) fontProgram;
            if (ttf.isFontSpecific()) {
                byte[] b = PdfEncodings.convertToBytes(text, "symboltt");
                if (b.length > 0) {
                    Glyph glyph = fontProgram.getGlyph(b[0] & 0xff);
                    if (glyph != null) {
                        glyphs.add(glyph);
                    }
                }
            } else {
                int ch;
                if (TextUtil.isSurrogatePair(text, from)) {
                    ch = TextUtil.convertToUtf32(text, from);
                    process = 2;
                } else {
                    ch = text.charAt(from);
                }
                glyphs.add(getGlyph(ch));
            }
        } else {
            throw new PdfException("Font has no suitable cmap.");
        }
        return process;
    }

    private boolean isAppendableGlyph(Glyph glyph) {
        // If font is specific and glyph.getCode() = 0, unicode value will be also 0.
        // Character.isIdentifierIgnorable(0) gets true.
        return glyph.getCode() > 0 || TextUtil.isWhitespaceOrNonPrintable(glyph.getUnicode());
    }

    @Override
    public String decode(PdfString content) {
        return decodeIntoGlyphLine(content).toString();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public GlyphLine decodeIntoGlyphLine(PdfString characterCodes) {
        List<Glyph> glyphs = new ArrayList<>();
        appendDecodedCodesToGlyphsList(glyphs, characterCodes);
        return new GlyphLine(glyphs);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean appendDecodedCodesToGlyphsList(List<Glyph> list, PdfString characterCodes) {
        boolean allCodesDecoded = true;

        final boolean isToUnicodeEmbedded = embeddedToUnicode != null;
        final CMapEncoding cmap = getCmap();
        final FontProgram fontProgram = getFontProgram();
        final List<byte[]> codeSpaceRanges = isToUnicodeEmbedded ? embeddedToUnicode.getCodeSpaceRanges() : cmap.getCodeSpaceRanges();

        String charCodesSequence = characterCodes.getValue();
        // A sequence of one or more bytes shall be extracted from the string and matched against the codespace
        // ranges in the CMap. That is, the first byte shall be matched against 1-byte codespace ranges; if no match is
        // found, a second byte shall be extracted, and the 2-byte code shall be matched against 2-byte codespace
        // ranges. This process continues for successively longer codes until a match is found or all codespace ranges
        // have been tested. There will be at most one match because codespace ranges shall not overlap.
        for (int i = 0; i < charCodesSequence.length(); i++) {
            int code = 0;
            Glyph glyph = null;
            int codeSpaceMatchedLength = 1;
            for (int codeLength = 1; codeLength <= MAX_CID_CODE_LENGTH && i + codeLength <= charCodesSequence.length();
                    codeLength++) {
                code = (code << 8) + charCodesSequence.charAt(i + codeLength - 1);

                if (PdfType0Font.containsCodeInCodeSpaceRange(codeSpaceRanges, code, codeLength)) {
                    codeSpaceMatchedLength = codeLength;
                } else {
                    continue;
                }

                // According to paragraph 9.10.2 of PDF Specification ISO 32000-2, if toUnicode is embedded, it is
                // necessary to use it to map directly code points to unicode. If not embedded, use CMap to map code
                // points to CIDs and then CIDFont to map CIDs to unicode.
                int glyphCode = isToUnicodeEmbedded ? code : cmap.getCidCode(code);
                glyph = fontProgram.getGlyphByCode(glyphCode);
                if (glyph != null) {
                    i += codeLength - 1;
                    break;
                }
            }
            if (glyph == null) {
                Logger logger = LoggerFactory.getLogger(PdfType0Font.class);
                if (logger.isWarnEnabled()) {
                    StringBuilder failedCodes = new StringBuilder();
                    for (int codeLength = 1;
                            codeLength <= MAX_CID_CODE_LENGTH && i + codeLength <= charCodesSequence.length();
                            codeLength++) {
                        failedCodes.append((int) charCodesSequence.charAt(i + codeLength - 1)).append(" ");
                    }
                    logger.warn(MessageFormatUtil
                            .format(IoLogMessageConstant.COULD_NOT_FIND_GLYPH_WITH_CODE, failedCodes.toString()));
                }
                i += codeSpaceMatchedLength - 1;
            }
            if (glyph == null || glyph.getChars() == null) {
                list.add(new Glyph(0, fontProgram.getGlyphByCode(0).getWidth(), -1));
                allCodesDecoded = false;
            } else {
                list.add(glyph);
            }
        }
        return allCodesDecoded;
    }

    @Override
    public float getContentWidth(PdfString content) {
        float width = 0;
        GlyphLine glyphLine = decodeIntoGlyphLine(content);
        for (int i = glyphLine.getStart(); i < glyphLine.getEnd(); i++) {
            width += glyphLine.get(i).getWidth();
        }
        return width;
    }

    @Override
    public boolean isBuiltWith(String fontProgram, String encoding) {
        return getFontProgram().isBuiltWith(fontProgram)
                && cmapEncoding.isBuiltWith(normalizeEncoding(encoding));
    }

    @Override
    public void flush() {
        if (isFlushed()) return;
        ensureUnderlyingObjectHasIndirectReference();
        if (newFont) {
            flushFontData();
        }
        super.flush();
    }

    /**
     * Gets CMAP associated with the Pdf Font.
     *
     * @return CMAP
     * @see CMapEncoding
     */
    public CMapEncoding getCmap() {
        return cmapEncoding;
    }

    @Override
    protected PdfDictionary getFontDescriptor(String fontName) {
        PdfDictionary fontDescriptor = new PdfDictionary();
        makeObjectIndirect(fontDescriptor);
        fontDescriptor.put(PdfName.Type, PdfName.FontDescriptor);
        fontDescriptor.put(PdfName.FontName, new PdfName(fontName));
        fontDescriptor.put(PdfName.FontBBox, new PdfArray(getFontProgram().getFontMetrics().getBbox()));
        fontDescriptor.put(PdfName.Ascent, new PdfNumber(getFontProgram().getFontMetrics().getTypoAscender()));
        fontDescriptor.put(PdfName.Descent, new PdfNumber(getFontProgram().getFontMetrics().getTypoDescender()));
        fontDescriptor.put(PdfName.CapHeight, new PdfNumber(getFontProgram().getFontMetrics().getCapHeight()));
        fontDescriptor.put(PdfName.ItalicAngle, new PdfNumber(getFontProgram().getFontMetrics().getItalicAngle()));
        fontDescriptor.put(PdfName.StemV, new PdfNumber(getFontProgram().getFontMetrics().getStemV()));
        fontDescriptor.put(PdfName.Flags, new PdfNumber(getFontProgram().getPdfFontFlags()));
        if (fontProgram.getFontIdentification().getPanose() != null) {
            PdfDictionary styleDictionary = new PdfDictionary();
            styleDictionary.put(PdfName.Panose, new PdfString(fontProgram.getFontIdentification().getPanose()).setHexWriting(true));
            fontDescriptor.put(PdfName.Style, styleDictionary);
        }
        return fontDescriptor;
    }

    private void convertToBytes(Glyph glyph, ByteBuffer result) {
        // NOTE: this should only ever be called with the identity CMap in RES-403
        int code = glyph.getCode();
        usedGlyphs.add(code);
        cmapEncoding.fillCmapBytes(code, result);
    }

    private static String getOrdering(PdfDictionary cidFont) {
        PdfDictionary cidinfo = cidFont.getAsDictionary(PdfName.CIDSystemInfo);
        if (cidinfo == null)
            return null;
        return cidinfo.containsKey(PdfName.Ordering) ? cidinfo.get(PdfName.Ordering).toString() : null;
    }

    private static boolean containsCodeInCodeSpaceRange(List<byte[]> codeSpaceRanges, int code, int length) {
        long unsignedCode = code & 0xffffffff;
        for (int i = 0; i < codeSpaceRanges.size(); i += 2) {
            if (length == codeSpaceRanges.get(i).length) {
                byte[] low = codeSpaceRanges.get(i);
                byte[] high = codeSpaceRanges.get(i + 1);
                long lowValue = bytesToLong(low);
                long highValue = bytesToLong(high);
                if (unsignedCode >= lowValue && unsignedCode <= highValue) {
                    return true;
                }
            }
        }
        return false;
    }

    private static long bytesToLong(byte[] bytes) {
        long res = 0;
        int shift = 0;
        for (int i = bytes.length - 1; i >= 0; --i) {
            res += (bytes[i] & 0xff) << shift;
            shift += 8;
        }

        return res;
    }

    private void flushFontData() {
        if (cidFontType == CID_FONT_TYPE_0) {
            getPdfObject().put(PdfName.Type, PdfName.Font);
            getPdfObject().put(PdfName.Subtype, PdfName.Type0);
            String name = fontProgram.getFontNames().getFontName();
            String style = fontProgram.getFontNames().getStyle();
            if (style.length() > 0) {
                name += "-" + style;
            }
            getPdfObject().put(PdfName.BaseFont, new PdfName(MessageFormatUtil.format("{0}-{1}", name, cmapEncoding.getCmapName())));
            getPdfObject().put(PdfName.Encoding, new PdfName(cmapEncoding.getCmapName()));
            PdfDictionary fontDescriptor = getFontDescriptor(name);
            PdfDictionary cidFont = getCidFont(fontDescriptor, fontProgram.getFontNames().getFontName(), false);
            getPdfObject().put(PdfName.DescendantFonts, new PdfArray(cidFont));

            fontDescriptor.flush();
            cidFont.flush();
        } else if (cidFontType == CID_FONT_TYPE_2) {
            TrueTypeFont ttf = (TrueTypeFont) getFontProgram();
            String fontName = updateSubsetPrefix(ttf.getFontNames().getFontName(), subset, embedded);
            PdfDictionary fontDescriptor = getFontDescriptor(fontName);

            PdfStream fontStream;
            ttf.updateUsedGlyphs((SortedSet<Integer>) usedGlyphs, subset, subsetRanges);
            if (ttf.isCff()) {
                byte[] cffBytes;
                if (subset) {
                    byte[] bytes = ttf.getFontStreamBytes();
                    Set<Integer> usedGids = ttf.mapGlyphsCidsToGids(usedGlyphs);
                    cffBytes = new CFFFontSubset(bytes, usedGids).Process();
                } else {
                    cffBytes = ttf.getFontStreamBytes();
                }
                fontStream = getPdfFontStream(cffBytes, new int[]{cffBytes.length});
                fontStream.put(PdfName.Subtype, new PdfName("CIDFontType0C"));
                // The PDF Reference manual advises to add -cmap in case CIDFontType0
                getPdfObject().put(PdfName.BaseFont,
                        new PdfName(MessageFormatUtil.format("{0}-{1}", fontName, cmapEncoding.getCmapName())));
                fontDescriptor.put(PdfName.FontFile3, fontStream);
            } else {
                byte[] ttfBytes = null;
                //getDirectoryOffset() > 0 means ttc, which shall be subsetted anyway.
                if (subset || ttf.getDirectoryOffset() > 0) {
                    try {
                        ttfBytes = ttf.getSubset(usedGlyphs, subset);
                    } catch (com.itextpdf.io.exceptions.IOException e) {
                        Logger logger = LoggerFactory.getLogger(PdfType0Font.class);
                        logger.warn(IoLogMessageConstant.FONT_SUBSET_ISSUE);
                        ttfBytes = null;
                    }
                }
                if (ttfBytes == null) {
                    ttfBytes = ttf.getFontStreamBytes();
                }
                fontStream = getPdfFontStream(ttfBytes, new int[]{ttfBytes.length});
                getPdfObject().put(PdfName.BaseFont, new PdfName(fontName));
                fontDescriptor.put(PdfName.FontFile2, fontStream);
            }

            // CIDSet shall be based on font.numberOfGlyphs property of the font, it is maxp.numGlyphs for ttf,
            // because technically we convert all unused glyphs to space, e.g. just remove outlines.
            int numOfGlyphs = ttf.getFontMetrics().getNumberOfGlyphs();
            byte[] cidSetBytes = new byte[ttf.getFontMetrics().getNumberOfGlyphs() / 8 + 1];
            for (int i = 0; i < numOfGlyphs / 8; i++) {
                cidSetBytes[i] |= 0xff;
            }
            for (int i = 0; i < numOfGlyphs % 8; i++) {
                cidSetBytes[cidSetBytes.length - 1] |= rotbits[i];
            }
            fontDescriptor.put(PdfName.CIDSet, new PdfStream(cidSetBytes));
            PdfDictionary cidFont = getCidFont(fontDescriptor, fontName, !ttf.isCff());

            getPdfObject().put(PdfName.Type, PdfName.Font);
            getPdfObject().put(PdfName.Subtype, PdfName.Type0);
            getPdfObject().put(PdfName.Encoding, new PdfName(cmapEncoding.getCmapName()));
            getPdfObject().put(PdfName.DescendantFonts, new PdfArray(cidFont));

            PdfStream toUnicode = getToUnicode();
            if (toUnicode != null) {
                getPdfObject().put(PdfName.ToUnicode, toUnicode);
                if (toUnicode.getIndirectReference() != null) {
                    toUnicode.flush();
                }
            }

            // getPdfObject().getIndirectReference() != null by assertion of PdfType0Font#flush()
            // This means, that fontDescriptor, cidFont and fontStream already are indirects
            if (getPdfObject().getIndirectReference().getDocument().getPdfVersion().compareTo(PdfVersion.PDF_2_0) >= 0) {
                // CIDSet is deprecated in PDF 2.0
                fontDescriptor.remove(PdfName.CIDSet);
            }
            fontDescriptor.flush();
            cidFont.flush();
            fontStream.flush();
        } else {
            throw new IllegalStateException("Unsupported CID Font");
        }
    }

    /**
     * Generates the CIDFontType2 dictionary.
     *
     * @param fontDescriptor the font descriptor dictionary
     * @param fontName       a name of the font
     * @param isType2        true, if the font is CIDFontType2 (TrueType glyphs),
     *                       otherwise false, i.e. CIDFontType0 (Type1/CFF glyphs)
     * @return fully initialized CIDFont
     */
    protected PdfDictionary getCidFont(PdfDictionary fontDescriptor, String fontName, boolean isType2) {
        PdfDictionary cidFont = new PdfDictionary();
        markObjectAsIndirect(cidFont);
        cidFont.put(PdfName.Type, PdfName.Font);
        // sivan; cff
        cidFont.put(PdfName.FontDescriptor, fontDescriptor);
        if (isType2) {
            cidFont.put(PdfName.Subtype, PdfName.CIDFontType2);
            cidFont.put(PdfName.CIDToGIDMap, PdfName.Identity);
        } else {
            cidFont.put(PdfName.Subtype, PdfName.CIDFontType0);
        }
        cidFont.put(PdfName.BaseFont, new PdfName(fontName));
        PdfDictionary cidInfo = new PdfDictionary();
        cidInfo.put(PdfName.Registry, new PdfString(cmapEncoding.getRegistry()));
        cidInfo.put(PdfName.Ordering, new PdfString(cmapEncoding.getOrdering()));
        cidInfo.put(PdfName.Supplement, new PdfNumber(cmapEncoding.getSupplement()));
        cidFont.put(PdfName.CIDSystemInfo, cidInfo);
        if (!vertical) {
            cidFont.put(PdfName.DW, new PdfNumber(FontProgram.DEFAULT_WIDTH));
            PdfObject widthsArray = generateWidthsArray();
            if (widthsArray != null) {
                cidFont.put(PdfName.W, widthsArray);
            }
        } else {
            // TODO DEVSIX-31
            Logger logger = LoggerFactory.getLogger(PdfType0Font.class);
            logger.warn("Vertical writing has not been implemented yet.");
        }
        return cidFont;
    }

    private PdfObject generateWidthsArray() {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        HighPrecisionOutputStream<ByteArrayOutputStream> stream = new HighPrecisionOutputStream<>(bytes);
        stream.writeByte('[');
        int lastNumber = -10;
        boolean firstTime = true;
        for (int code : usedGlyphs) {
            Glyph glyph = fontProgram.getGlyphByCode(code);
            if (glyph.getWidth() == FontProgram.DEFAULT_WIDTH) {
                continue;
            }
            if (glyph.getCode() == lastNumber + 1) {
                stream.writeByte(' ');
            } else {
                if (!firstTime) {
                    stream.writeByte(']');
                }
                firstTime = false;
                stream.writeInteger(glyph.getCode());
                stream.writeByte('[');
            }
            stream.writeInteger(glyph.getWidth());
            lastNumber = glyph.getCode();
        }
        if (stream.getCurrentPos() > 1) {
            stream.writeString("]]");
            return new PdfLiteral(bytes.toByteArray());
        }
        return null;
    }

    /**
     * Creates a ToUnicode CMap to allow copy and paste from Acrobat.
     *
     * @return the stream representing this CMap or <CODE>null</CODE>
     */
    public PdfStream getToUnicode() {
        HighPrecisionOutputStream<ByteArrayOutputStream> stream =
                new HighPrecisionOutputStream<>(new ByteArrayOutputStream());
        stream.writeString("/CIDInit /ProcSet findresource begin\n" +
                "12 dict begin\n" +
                "begincmap\n" +
                "/CIDSystemInfo\n" +
                "<< /Registry (Adobe)\n" +
                "/Ordering (UCS)\n" +
                "/Supplement 0\n" +
                ">> def\n" +
                "/CMapName /Adobe-Identity-UCS def\n" +
                "/CMapType 2 def\n" +
                "1 begincodespacerange\n" +
                "<0000><FFFF>\n" +
                "endcodespacerange\n");

        //accumulate long tag into a subset and write it.
        ArrayList<Glyph> glyphGroup = new ArrayList<>(100);

        int bfranges = 0;
        for (Integer glyphId : usedGlyphs) {
            Glyph glyph = fontProgram.getGlyphByCode((int) glyphId);
            if (glyph.getChars() != null) {
                glyphGroup.add(glyph);
                if (glyphGroup.size() == 100) {
                    bfranges += writeBfrange(stream, glyphGroup);
                }
            }
        }
        //flush leftovers
        bfranges += writeBfrange(stream, glyphGroup);

        if (bfranges == 0)
            return null;

        stream.writeString("endcmap\n" +
                "CMapName currentdict /CMap defineresource pop\n" +
                "end end\n");
        return new PdfStream(((ByteArrayOutputStream)stream.getOutputStream()).toByteArray());
    }

    private static int writeBfrange(HighPrecisionOutputStream<ByteArrayOutputStream> stream, List<Glyph> range) {
        if (range.isEmpty()) return 0;
        stream.writeInteger(range.size());
        stream.writeString(" beginbfrange\n");
        for (Glyph glyph: range) {
            String fromTo = CMapContentParser.toHex(glyph.getCode());
            stream.writeString(fromTo);
            stream.writeString(fromTo);
            stream.writeByte('<');
            for (char ch : glyph.getChars()) {
                stream.writeString(toHex4(ch));
            }
            stream.writeByte('>');
            stream.writeByte('\n');
        }
        stream.writeString("endbfrange\n");
        range.clear();
        return 1;
    }

    private static String toHex4(char ch) {
        String s = "0000" + Integer.toHexString(ch);
        return s.substring(s.length() - 4);
    }

    private String getCompatibleUniMap(String registry) {
        String uniMap = "";
        for (String name : CidFontProperties.getRegistryNames().get(registry + "_Uni")) {
            uniMap = name;
            if (name.endsWith("V") && vertical) {
                break;
            } else if (!name.endsWith("V") && !vertical) {
                break;
            }
        }
        return uniMap;
    }

    private static CMapEncoding createCMap(PdfObject cmap, String uniMap) {
        if (cmap.isStream()) {
            PdfStream cmapStream = (PdfStream) cmap;
            byte[] cmapBytes = cmapStream.getBytes();
            return new CMapEncoding(cmapStream.getAsName(PdfName.CMapName).getValue(), cmapBytes);
        } else {
            String cmapName = ((PdfName) cmap).getValue();
            if (PdfEncodings.IDENTITY_H.equals(cmapName) || PdfEncodings.IDENTITY_V.equals(cmapName)) {
                return new CMapEncoding(cmapName);
            } else {
                return new CMapEncoding(cmapName, uniMap);
            }
        }
    }

    private static String normalizeEncoding(String encoding) {
        return null == encoding || DEFAULT_ENCODING.equals(encoding)
                ? PdfEncodings.IDENTITY_H
                : encoding;
    }
}