//========================================================================
//
// FoFiTrueType.cc
//
// Copyright 1999-2003 Glyph & Cog, LLC
//
//========================================================================

//========================================================================
//
// Modified under the Poppler project - http://poppler.freedesktop.org
//
// All changes made under the Poppler project to this file are licensed
// under GPL version 2 or later
//
// Copyright (C) 2006 Takashi Iwai <tiwai@suse.de>
// Copyright (C) 2007 Koji Otani <sho@bbr.jp>
// Copyright (C) 2007 Carlos Garcia Campos <carlosgc@gnome.org>
// Copyright (C) 2008, 2009, 2012, 2014-2022, 2024 Albert Astals Cid <aacid@kde.org>
// Copyright (C) 2008 Tomas Are Haavet <tomasare@gmail.com>
// Copyright (C) 2012 Suzuki Toshiya <mpsuzuki@hiroshima-u.ac.jp>
// Copyright (C) 2012, 2017 Adrian Johnson <ajohnson@redneon.com>
// Copyright (C) 2014 Thomas Freitag <Thomas.Freitag@alfa.de>
// Copyright (C) 2015 Aleksei Volkov <Aleksei Volkov>
// Copyright (C) 2015, 2016 William Bader <williambader@hotmail.com>
// Copyright (C) 2018 Adam Reichold <adam.reichold@t-online.de>
// Copyright (C) 2022 Zachary Travis <ztravis@everlaw.com>
// Copyright (C) 2022, 2024 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright (C) 2024 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
//
// To see a description of the changes please see the Changelog file that
// came with your tarball or type make ChangeLog if you are building from git
//
//========================================================================

#include <config.h>

#include <cstdlib>
#include <cstring>
#include <climits>
#include <algorithm>
#include <array>
#include "goo/GooLikely.h"
#include "goo/GooString.h"
#include "goo/GooCheckedOps.h"
#include "FoFiType1C.h"
#include "FoFiTrueType.h"
#include "poppler/Error.h"

//
// Terminology
// -----------
//
// character code = number used as an element of a text string
//
// character name = glyph name = name for a particular glyph within a
//                  font
//
// glyph index = GID = position (within some internal table in the font)
//               where the instructions to draw a particular glyph are
//               stored
//
// Type 1 fonts
// ------------
//
// Type 1 fonts contain:
//
// Encoding: array of glyph names, maps char codes to glyph names
//
//           Encoding[charCode] = charName
//
// CharStrings: dictionary of instructions, keyed by character names,
//              maps character name to glyph data
//
//              CharStrings[charName] = glyphData
//
// TrueType fonts
// --------------
//
// TrueType fonts contain:
//
// 'cmap' table: mapping from character code to glyph index; there may
//               be multiple cmaps in a TrueType font
//
//               cmap[charCode] = gid
//
// 'post' table: mapping from glyph index to glyph name
//
//               post[gid] = glyphName
//
// Type 42 fonts
// -------------
//
// Type 42 fonts contain:
//
// Encoding: array of glyph names, maps char codes to glyph names
//
//           Encoding[charCode] = charName
//
// CharStrings: dictionary of glyph indexes, keyed by character names,
//              maps character name to glyph index
//
//              CharStrings[charName] = gid
//

//------------------------------------------------------------------------

#define ttcfTag 0x74746366

//------------------------------------------------------------------------

struct TrueTypeTable
{
    unsigned int tag = 0;
    unsigned int checksum = 0;
    int offset = 0;
    int origOffset = 0;
    int len = 0;
};

struct TrueTypeCmap
{
    int platform = 0;
    int encoding = 0;
    int offset = 0;
    int len = 0;
    int fmt = 0;
};

struct TrueTypeLoca
{
    int idx = 0;
    int origOffset = 0;
    int newOffset = 0;
    int len = 0;
};

#define cmapTag 0x636d6170
#define glyfTag 0x676c7966
#define headTag 0x68656164
#define hheaTag 0x68686561
#define hmtxTag 0x686d7478
#define locaTag 0x6c6f6361
#define nameTag 0x6e616d65
#define os2Tag 0x4f532f32
#define postTag 0x706f7374
#define vrt2Tag 0x76727432
#define vertTag 0x76657274

struct cmpTrueTypeLocaOffsetFunctor
{
    bool operator()(const TrueTypeLoca loca1, const TrueTypeLoca loca2)
    {
        if (loca1.origOffset == loca2.origOffset) {
            return loca1.idx < loca2.idx;
        }
        return loca1.origOffset < loca2.origOffset;
    }
};

struct cmpTrueTypeLocaIdxFunctor
{
    bool operator()(const TrueTypeLoca loca1, const TrueTypeLoca loca2) { return loca1.idx < loca2.idx; }
};

struct cmpTrueTypeTableTagFunctor
{
    bool operator()(const TrueTypeTable &tab1, const TrueTypeTable &tab2) { return tab1.tag < tab2.tag; }
};

//------------------------------------------------------------------------

struct T42Table
{
    const char *tag; // 4-byte tag
    bool required; // required by the TrueType spec?
};

// TrueType tables to be embedded in Type 42 fonts.
// NB: the table names must be in alphabetical order here.
#define nT42Tables 11
static const T42Table t42Tables[nT42Tables] = { { "cvt ", true }, { "fpgm", true }, { "glyf", true }, { "head", true },  { "hhea", true }, { "hmtx", true },
                                                { "loca", true }, { "maxp", true }, { "prep", true }, { "vhea", false }, { "vmtx", false } };
#define t42HeadTable 3
#define t42LocaTable 6
#define t42GlyfTable 2
#define t42VheaTable 9
#define t42VmtxTable 10

//------------------------------------------------------------------------

// Glyph names in some arbitrary standard order that Apple uses for
// their TrueType fonts.
static const char *macGlyphNames[258] = { ".notdef",
                                          "null",
                                          "CR",
                                          "space",
                                          "exclam",
                                          "quotedbl",
                                          "numbersign",
                                          "dollar",
                                          "percent",
                                          "ampersand",
                                          "quotesingle",
                                          "parenleft",
                                          "parenright",
                                          "asterisk",
                                          "plus",
                                          "comma",
                                          "hyphen",
                                          "period",
                                          "slash",
                                          "zero",
                                          "one",
                                          "two",
                                          "three",
                                          "four",
                                          "five",
                                          "six",
                                          "seven",
                                          "eight",
                                          "nine",
                                          "colon",
                                          "semicolon",
                                          "less",
                                          "equal",
                                          "greater",
                                          "question",
                                          "at",
                                          "A",
                                          "B",
                                          "C",
                                          "D",
                                          "E",
                                          "F",
                                          "G",
                                          "H",
                                          "I",
                                          "J",
                                          "K",
                                          "L",
                                          "M",
                                          "N",
                                          "O",
                                          "P",
                                          "Q",
                                          "R",
                                          "S",
                                          "T",
                                          "U",
                                          "V",
                                          "W",
                                          "X",
                                          "Y",
                                          "Z",
                                          "bracketleft",
                                          "backslash",
                                          "bracketright",
                                          "asciicircum",
                                          "underscore",
                                          "grave",
                                          "a",
                                          "b",
                                          "c",
                                          "d",
                                          "e",
                                          "f",
                                          "g",
                                          "h",
                                          "i",
                                          "j",
                                          "k",
                                          "l",
                                          "m",
                                          "n",
                                          "o",
                                          "p",
                                          "q",
                                          "r",
                                          "s",
                                          "t",
                                          "u",
                                          "v",
                                          "w",
                                          "x",
                                          "y",
                                          "z",
                                          "braceleft",
                                          "bar",
                                          "braceright",
                                          "asciitilde",
                                          "Adieresis",
                                          "Aring",
                                          "Ccedilla",
                                          "Eacute",
                                          "Ntilde",
                                          "Odieresis",
                                          "Udieresis",
                                          "aacute",
                                          "agrave",
                                          "acircumflex",
                                          "adieresis",
                                          "atilde",
                                          "aring",
                                          "ccedilla",
                                          "eacute",
                                          "egrave",
                                          "ecircumflex",
                                          "edieresis",
                                          "iacute",
                                          "igrave",
                                          "icircumflex",
                                          "idieresis",
                                          "ntilde",
                                          "oacute",
                                          "ograve",
                                          "ocircumflex",
                                          "odieresis",
                                          "otilde",
                                          "uacute",
                                          "ugrave",
                                          "ucircumflex",
                                          "udieresis",
                                          "dagger",
                                          "degree",
                                          "cent",
                                          "sterling",
                                          "section",
                                          "bullet",
                                          "paragraph",
                                          "germandbls",
                                          "registered",
                                          "copyright",
                                          "trademark",
                                          "acute",
                                          "dieresis",
                                          "notequal",
                                          "AE",
                                          "Oslash",
                                          "infinity",
                                          "plusminus",
                                          "lessequal",
                                          "greaterequal",
                                          "yen",
                                          "mu",
                                          "partialdiff",
                                          "summation",
                                          "product",
                                          "pi",
                                          "integral",
                                          "ordfeminine",
                                          "ordmasculine",
                                          "Omega",
                                          "ae",
                                          "oslash",
                                          "questiondown",
                                          "exclamdown",
                                          "logicalnot",
                                          "radical",
                                          "florin",
                                          "approxequal",
                                          "increment",
                                          "guillemotleft",
                                          "guillemotright",
                                          "ellipsis",
                                          "nbspace",
                                          "Agrave",
                                          "Atilde",
                                          "Otilde",
                                          "OE",
                                          "oe",
                                          "endash",
                                          "emdash",
                                          "quotedblleft",
                                          "quotedblright",
                                          "quoteleft",
                                          "quoteright",
                                          "divide",
                                          "lozenge",
                                          "ydieresis",
                                          "Ydieresis",
                                          "fraction",
                                          "currency",
                                          "guilsinglleft",
                                          "guilsinglright",
                                          "fi",
                                          "fl",
                                          "daggerdbl",
                                          "periodcentered",
                                          "quotesinglbase",
                                          "quotedblbase",
                                          "perthousand",
                                          "Acircumflex",
                                          "Ecircumflex",
                                          "Aacute",
                                          "Edieresis",
                                          "Egrave",
                                          "Iacute",
                                          "Icircumflex",
                                          "Idieresis",
                                          "Igrave",
                                          "Oacute",
                                          "Ocircumflex",
                                          "applelogo",
                                          "Ograve",
                                          "Uacute",
                                          "Ucircumflex",
                                          "Ugrave",
                                          "dotlessi",
                                          "circumflex",
                                          "tilde",
                                          "overscore",
                                          "breve",
                                          "dotaccent",
                                          "ring",
                                          "cedilla",
                                          "hungarumlaut",
                                          "ogonek",
                                          "caron",
                                          "Lslash",
                                          "lslash",
                                          "Scaron",
                                          "scaron",
                                          "Zcaron",
                                          "zcaron",
                                          "brokenbar",
                                          "Eth",
                                          "eth",
                                          "Yacute",
                                          "yacute",
                                          "Thorn",
                                          "thorn",
                                          "minus",
                                          "multiply",
                                          "onesuperior",
                                          "twosuperior",
                                          "threesuperior",
                                          "onehalf",
                                          "onequarter",
                                          "threequarters",
                                          "franc",
                                          "Gbreve",
                                          "gbreve",
                                          "Idot",
                                          "Scedilla",
                                          "scedilla",
                                          "Cacute",
                                          "cacute",
                                          "Ccaron",
                                          "ccaron",
                                          "dmacron" };

//------------------------------------------------------------------------
// FoFiTrueType
//------------------------------------------------------------------------

std::unique_ptr<FoFiTrueType> FoFiTrueType::make(const unsigned char *fileA, int lenA, int faceIndexA)
{
    // Cannot use std::make_unique, because the constructor is private
    auto ff = new FoFiTrueType(fileA, lenA, false, faceIndexA);
    if (!ff->parsedOk) {
        delete ff;
        return nullptr;
    }
    return std::unique_ptr<FoFiTrueType>(ff);
}

std::unique_ptr<FoFiTrueType> FoFiTrueType::load(const char *fileName, int faceIndexA)
{
    char *fileA;
    int lenA;

    if (!(fileA = FoFiBase::readFile(fileName, &lenA))) {
        return nullptr;
    }
    // Cannot use std::make_unique, because the constructor is private
    auto ff = new FoFiTrueType((unsigned char *)fileA, lenA, true, faceIndexA);
    if (!ff->parsedOk) {
        delete ff;
        return nullptr;
    }
    return std::unique_ptr<FoFiTrueType>(ff);
}

FoFiTrueType::FoFiTrueType(const unsigned char *fileA, int lenA, bool freeFileDataA, int faceIndexA) : FoFiBase(fileA, lenA, freeFileDataA)
{
    parsedOk = false;
    faceIndex = faceIndexA;
    gsubFeatureTable = 0;
    gsubLookupList = 0;

    parse();
}

FoFiTrueType::~FoFiTrueType() = default;

int FoFiTrueType::getNumCmaps() const
{
    return cmaps.size();
}

int FoFiTrueType::getCmapPlatform(int i) const
{
    return cmaps[i].platform;
}

int FoFiTrueType::getCmapEncoding(int i) const
{
    return cmaps[i].encoding;
}

int FoFiTrueType::findCmap(int platform, int encoding) const
{
    for (int i = 0; i < (int)cmaps.size(); ++i) {
        if (cmaps[i].platform == platform && cmaps[i].encoding == encoding) {
            return i;
        }
    }
    return -1;
}

int FoFiTrueType::mapCodeToGID(int i, unsigned int c) const
{
    int gid;
    unsigned int segCnt, segEnd, segStart, segDelta, segOffset;
    unsigned int cmapFirst, cmapLen;
    int pos, a, b, m;
    unsigned int high, low, idx;
    bool ok;

    if (i < 0 || i >= (int)cmaps.size()) {
        return 0;
    }
    ok = true;
    pos = cmaps[i].offset;
    switch (cmaps[i].fmt) {
    case 0:
        if (c + 6 >= (unsigned int)cmaps[i].len) {
            return 0;
        }
        gid = getU8(cmaps[i].offset + 6 + c, &ok);
        break;
    case 2:
        high = c >> 8;
        low = c & 0xFFU;
        idx = getU16BE(pos + 6 + high * 2, &ok);
        segStart = getU16BE(pos + 6 + 512 + idx, &ok);
        segCnt = getU16BE(pos + 6 + 512 + idx + 2, &ok);
        segDelta = getS16BE(pos + 6 + 512 + idx + 4, &ok);
        segOffset = getU16BE(pos + 6 + 512 + idx + 6, &ok);
        if (low < segStart || low >= segStart + segCnt) {
            gid = 0;
        } else {
            int val = getU16BE(pos + 6 + 512 + idx + 6 + segOffset + (low - segStart) * 2, &ok);
            gid = val == 0 ? 0 : (val + segDelta) & 0xFFFFU;
        }
        break;
    case 4:
        segCnt = getU16BE(pos + 6, &ok) / 2;
        a = -1;
        b = segCnt - 1;
        segEnd = getU16BE(pos + 14 + 2 * b, &ok);
        if (c > segEnd) {
            // malformed font -- the TrueType spec requires the last segEnd
            // to be 0xffff
            return 0;
        }
        // invariant: seg[a].end < code <= seg[b].end
        while (b - a > 1 && ok) {
            m = (a + b) / 2;
            segEnd = getU16BE(pos + 14 + 2 * m, &ok);
            if (segEnd < c) {
                a = m;
            } else {
                b = m;
            }
        }
        segStart = getU16BE(pos + 16 + 2 * segCnt + 2 * b, &ok);
        segDelta = getU16BE(pos + 16 + 4 * segCnt + 2 * b, &ok);
        segOffset = getU16BE(pos + 16 + 6 * segCnt + 2 * b, &ok);
        if (c < segStart) {
            return 0;
        }
        if (segOffset == 0) {
            gid = (c + segDelta) & 0xffff;
        } else {
            gid = getU16BE(pos + 16 + 6 * segCnt + 2 * b + segOffset + 2 * (c - segStart), &ok);
            if (gid != 0) {
                gid = (gid + segDelta) & 0xffff;
            }
        }
        break;
    case 6:
        cmapFirst = getU16BE(pos + 6, &ok);
        cmapLen = getU16BE(pos + 8, &ok);
        if (c < cmapFirst || c >= cmapFirst + cmapLen) {
            return 0;
        }
        gid = getU16BE(pos + 10 + 2 * (c - cmapFirst), &ok);
        break;
    case 12:
    case 13:
        segCnt = getU32BE(pos + 12, &ok);
        a = -1;
        b = segCnt - 1;
        segEnd = getU32BE(pos + 16 + 12 * b + 4, &ok);
        if (c > segEnd) {
            return 0;
        }
        // invariant: seg[a].end < code <= seg[b].end
        while (b - a > 1 && ok) {
            m = (a + b) / 2;
            segEnd = getU32BE(pos + 16 + 12 * m + 4, &ok);
            if (segEnd < c) {
                a = m;
            } else {
                b = m;
            }
        }
        segStart = getU32BE(pos + 16 + 12 * b, &ok);
        segDelta = getU32BE(pos + 16 + 12 * b + 8, &ok);
        if (c < segStart) {
            return 0;
        }
        // In format 12, the glyph codes increment through
        // each segment; in format 13 the same glyph code is used
        // for an entire segment.
        gid = segDelta + (cmaps[i].fmt == 12 ? (c - segStart) : 0);
        break;
    default:
        return 0;
    }
    if (!ok) {
        return 0;
    }
    return gid;
}

int FoFiTrueType::mapNameToGID(const char *name) const
{
    const auto gid = nameToGID.find(name);
    if (gid == nameToGID.end()) {
        return 0;
    }
    return gid->second;
}

bool FoFiTrueType::getCFFBlock(char **start, int *length) const
{
    if (!openTypeCFF || tables.empty()) {
        return false;
    }
    int i = seekTable("CFF ");
    if (i < 0 || !checkRegion(tables[i].offset, tables[i].len)) {
        return false;
    }
    *start = (char *)file + tables[i].offset;
    *length = tables[i].len;
    return true;
}

int *FoFiTrueType::getCIDToGIDMap(int *nCIDs) const
{
    char *start;
    int length;
    FoFiType1C *ff;
    int *map;

    *nCIDs = 0;
    if (!getCFFBlock(&start, &length)) {
        return nullptr;
    }
    if (!(ff = FoFiType1C::make((unsigned char *)start, length))) {
        return nullptr;
    }
    map = ff->getCIDToGIDMap(nCIDs);
    delete ff;
    return map;
}

int FoFiTrueType::getEmbeddingRights() const
{
    int i, fsType;
    bool ok;

    if ((i = seekTable("OS/2")) < 0) {
        return 4;
    }
    ok = true;
    fsType = getU16BE(tables[i].offset + 8, &ok);
    if (!ok) {
        return 4;
    }
    if (fsType & 0x0008) {
        return 2;
    }
    if (fsType & 0x0004) {
        return 1;
    }
    if (fsType & 0x0002) {
        return 0;
    }
    return 3;
}

void FoFiTrueType::getFontMatrix(double *mat) const
{
    char *start;
    int length;
    FoFiType1C *ff;

    if (!getCFFBlock(&start, &length)) {
        return;
    }
    if (!(ff = FoFiType1C::make((unsigned char *)start, length))) {
        return;
    }
    ff->getFontMatrix(mat);
    delete ff;
}

void FoFiTrueType::convertToType42(const char *psName, char **encoding, int *codeToGID, FoFiOutputFunc outputFunc, void *outputStream) const
{
    int maxUsedGlyph;
    bool ok;

    if (openTypeCFF) {
        return;
    }

    // write the header
    ok = true;
    std::string buf = GooString::format("%!PS-TrueTypeFont-{0:2g}\n", (double)getS32BE(0, &ok) / 65536.0);
    (*outputFunc)(outputStream, buf.c_str(), buf.size());

    // begin the font dictionary
    (*outputFunc)(outputStream, "10 dict begin\n", 14);
    (*outputFunc)(outputStream, "/FontName /", 11);
    (*outputFunc)(outputStream, psName, strlen(psName));
    (*outputFunc)(outputStream, " def\n", 5);
    (*outputFunc)(outputStream, "/FontType 42 def\n", 17);
    (*outputFunc)(outputStream, "/FontMatrix [1 0 0 1 0 0] def\n", 30);
    buf = GooString::format("/FontBBox [{0:d} {1:d} {2:d} {3:d}] def\n", bbox[0], bbox[1], bbox[2], bbox[3]);
    (*outputFunc)(outputStream, buf.c_str(), buf.size());
    (*outputFunc)(outputStream, "/PaintType 0 def\n", 17);

    // write the guts of the dictionary
    cvtEncoding(encoding, outputFunc, outputStream);
    cvtCharStrings(encoding, codeToGID, outputFunc, outputStream);
    cvtSfnts(outputFunc, outputStream, nullptr, false, &maxUsedGlyph);

    // end the dictionary and define the font
    (*outputFunc)(outputStream, "FontName currentdict end definefont pop\n", 40);
}

void FoFiTrueType::convertToType1(const char *psName, const char **newEncoding, bool ascii, FoFiOutputFunc outputFunc, void *outputStream) const
{
    char *start;
    int length;
    FoFiType1C *ff;

    if (!getCFFBlock(&start, &length)) {
        return;
    }
    if (!(ff = FoFiType1C::make((unsigned char *)start, length))) {
        return;
    }
    ff->convertToType1(psName, newEncoding, ascii, outputFunc, outputStream);
    delete ff;
}

void FoFiTrueType::convertToCIDType2(const char *psName, const int *cidMap, int nCIDs, bool needVerticalMetrics, FoFiOutputFunc outputFunc, void *outputStream) const
{
    int cid, maxUsedGlyph;
    bool ok;
    int i, j, k;

    if (openTypeCFF) {
        return;
    }

    // write the header
    ok = true;
    std::string buf = GooString::format("%!PS-TrueTypeFont-{0:2g}\n", (double)getS32BE(0, &ok) / 65536.0);
    (*outputFunc)(outputStream, buf.c_str(), buf.size());

    // begin the font dictionary
    (*outputFunc)(outputStream, "20 dict begin\n", 14);
    (*outputFunc)(outputStream, "/CIDFontName /", 14);
    (*outputFunc)(outputStream, psName, strlen(psName));
    (*outputFunc)(outputStream, " def\n", 5);
    (*outputFunc)(outputStream, "/CIDFontType 2 def\n", 19);
    (*outputFunc)(outputStream, "/FontType 42 def\n", 17);
    (*outputFunc)(outputStream, "/CIDSystemInfo 3 dict dup begin\n", 32);
    (*outputFunc)(outputStream, "  /Registry (Adobe) def\n", 24);
    (*outputFunc)(outputStream, "  /Ordering (Identity) def\n", 27);
    (*outputFunc)(outputStream, "  /Supplement 0 def\n", 20);
    (*outputFunc)(outputStream, "  end def\n", 10);
    (*outputFunc)(outputStream, "/GDBytes 2 def\n", 15);
    if (cidMap) {
        buf = GooString::format("/CIDCount {0:d} def\n", nCIDs);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
        if (nCIDs > 32767) {
            (*outputFunc)(outputStream, "/CIDMap [", 9);
            for (i = 0; i < nCIDs; i += 32768 - 16) {
                (*outputFunc)(outputStream, "<\n", 2);
                for (j = 0; j < 32768 - 16 && i + j < nCIDs; j += 16) {
                    (*outputFunc)(outputStream, "  ", 2);
                    for (k = 0; k < 16 && i + j + k < nCIDs; ++k) {
                        cid = cidMap[i + j + k];
                        buf = GooString::format("{0:02x}{1:02x}", (cid >> 8) & 0xff, cid & 0xff);
                        (*outputFunc)(outputStream, buf.c_str(), buf.size());
                    }
                    (*outputFunc)(outputStream, "\n", 1);
                }
                (*outputFunc)(outputStream, "  >", 3);
            }
            (*outputFunc)(outputStream, "\n", 1);
            (*outputFunc)(outputStream, "] def\n", 6);
        } else {
            (*outputFunc)(outputStream, "/CIDMap <\n", 10);
            for (i = 0; i < nCIDs; i += 16) {
                (*outputFunc)(outputStream, "  ", 2);
                for (j = 0; j < 16 && i + j < nCIDs; ++j) {
                    cid = cidMap[i + j];
                    buf = GooString::format("{0:02x}{1:02x}", (cid >> 8) & 0xff, cid & 0xff);
                    (*outputFunc)(outputStream, buf.c_str(), buf.size());
                }
                (*outputFunc)(outputStream, "\n", 1);
            }
            (*outputFunc)(outputStream, "> def\n", 6);
        }
    } else {
        // direct mapping - just fill the string(s) with s[i]=i
        buf = GooString::format("/CIDCount {0:d} def\n", nGlyphs);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
        if (nGlyphs > 32767) {
            (*outputFunc)(outputStream, "/CIDMap [\n", 10);
            for (i = 0; i < nGlyphs; i += 32767) {
                j = nGlyphs - i < 32767 ? nGlyphs - i : 32767;
                buf = GooString::format("  {0:d} string 0 1 {1:d} {{\n", 2 * j, j - 1);
                (*outputFunc)(outputStream, buf.c_str(), buf.size());
                buf = GooString::format("    2 copy dup 2 mul exch {0:d} add -8 bitshift put\n", i);
                (*outputFunc)(outputStream, buf.c_str(), buf.size());
                buf = GooString::format("    1 index exch dup 2 mul 1 add exch {0:d} add"
                                        " 255 and put\n",
                                        i);
                (*outputFunc)(outputStream, buf.c_str(), buf.size());
                (*outputFunc)(outputStream, "  } for\n", 8);
            }
            (*outputFunc)(outputStream, "] def\n", 6);
        } else {
            buf = GooString::format("/CIDMap {0:d} string\n", 2 * nGlyphs);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
            buf = GooString::format("  0 1 {0:d} {{\n", nGlyphs - 1);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
            (*outputFunc)(outputStream, "    2 copy dup 2 mul exch -8 bitshift put\n", 42);
            (*outputFunc)(outputStream, "    1 index exch dup 2 mul 1 add exch 255 and put\n", 50);
            (*outputFunc)(outputStream, "  } for\n", 8);
            (*outputFunc)(outputStream, "def\n", 4);
        }
    }
    (*outputFunc)(outputStream, "/FontMatrix [1 0 0 1 0 0] def\n", 30);
    buf = GooString::format("/FontBBox [{0:d} {1:d} {2:d} {3:d}] def\n", bbox[0], bbox[1], bbox[2], bbox[3]);
    (*outputFunc)(outputStream, buf.c_str(), buf.size());
    (*outputFunc)(outputStream, "/PaintType 0 def\n", 17);
    (*outputFunc)(outputStream, "/Encoding [] readonly def\n", 26);
    (*outputFunc)(outputStream, "/CharStrings 1 dict dup begin\n", 30);
    (*outputFunc)(outputStream, "  /.notdef 0 def\n", 17);
    (*outputFunc)(outputStream, "  end readonly def\n", 19);

    // write the guts of the dictionary
    cvtSfnts(outputFunc, outputStream, nullptr, needVerticalMetrics, &maxUsedGlyph);

    // end the dictionary and define the font
    (*outputFunc)(outputStream, "CIDFontName currentdict end /CIDFont defineresource pop\n", 56);
}

void FoFiTrueType::convertToCIDType0(const char *psName, int *cidMap, int nCIDs, FoFiOutputFunc outputFunc, void *outputStream) const
{
    char *start;
    int length;
    FoFiType1C *ff;

    if (!getCFFBlock(&start, &length)) {
        return;
    }
    if (!(ff = FoFiType1C::make((unsigned char *)start, length))) {
        return;
    }
    ff->convertToCIDType0(psName, cidMap, nCIDs, outputFunc, outputStream);
    delete ff;
}

void FoFiTrueType::convertToType0(const char *psName, int *cidMap, int nCIDs, bool needVerticalMetrics, int *maxValidGlyph, FoFiOutputFunc outputFunc, void *outputStream) const
{
    GooString *sfntsName;
    int maxUsedGlyph, n, i, j;

    *maxValidGlyph = -1;

    if (openTypeCFF) {
        return;
    }

    // write the Type 42 sfnts array
    sfntsName = (new GooString(psName))->append("_sfnts");
    cvtSfnts(outputFunc, outputStream, sfntsName, needVerticalMetrics, &maxUsedGlyph);
    delete sfntsName;

    // write the descendant Type 42 fonts
    // (The following is a kludge: nGlyphs is the glyph count from the
    // maxp table; maxUsedGlyph is the max glyph number that has a
    // non-zero-length description, from the loca table.  The problem is
    // that some TrueType font subsets fail to change the glyph count,
    // i.e., nGlyphs is much larger than maxUsedGlyph+1, which results
    // in an unnecessarily huge Type 0 font.  But some other PDF files
    // have fonts with only zero or one used glyph, and a content stream
    // that refers to one of the unused glyphs -- this results in PS
    // errors if we simply use maxUsedGlyph+1 for the Type 0 font.  So
    // we compromise by always defining at least 256 glyphs.)
    // Some fonts have a large nGlyphs but maxUsedGlyph of 0.
    // These fonts might reference any glyph.
    // Return the last written glyph number in maxValidGlyph.
    // PSOutputDev::drawString() can use maxValidGlyph to avoid
    // referencing zero-length glyphs that we trimmed.
    // This allows pdftops to avoid writing huge files while still
    // handling the rare PDF that uses a zero-length glyph.
    if (cidMap) {
        n = nCIDs;
    } else if (nGlyphs > maxUsedGlyph + 256) {
        if (maxUsedGlyph <= 255) {
            n = 256;
        } else {
            n = maxUsedGlyph + 1;
        }
    } else {
        n = nGlyphs;
    }
    *maxValidGlyph = n - 1;
    for (i = 0; i < n; i += 256) {
        (*outputFunc)(outputStream, "10 dict begin\n", 14);
        (*outputFunc)(outputStream, "/FontName /", 11);
        (*outputFunc)(outputStream, psName, strlen(psName));
        std::string buf = GooString::format("_{0:02x} def\n", i >> 8);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
        (*outputFunc)(outputStream, "/FontType 42 def\n", 17);
        (*outputFunc)(outputStream, "/FontMatrix [1 0 0 1 0 0] def\n", 30);
        buf = GooString::format("/FontBBox [{0:d} {1:d} {2:d} {3:d}] def\n", bbox[0], bbox[1], bbox[2], bbox[3]);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
        (*outputFunc)(outputStream, "/PaintType 0 def\n", 17);
        (*outputFunc)(outputStream, "/sfnts ", 7);
        (*outputFunc)(outputStream, psName, strlen(psName));
        (*outputFunc)(outputStream, "_sfnts def\n", 11);
        (*outputFunc)(outputStream, "/Encoding 256 array\n", 20);
        for (j = 0; j < 256 && i + j < n; ++j) {
            buf = GooString::format("dup {0:d} /c{1:02x} put\n", j, j);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
        }
        (*outputFunc)(outputStream, "readonly def\n", 13);
        (*outputFunc)(outputStream, "/CharStrings 257 dict dup begin\n", 32);
        (*outputFunc)(outputStream, "/.notdef 0 def\n", 15);
        for (j = 0; j < 256 && i + j < n; ++j) {
            buf = GooString::format("/c{0:02x} {1:d} def\n", j, cidMap ? cidMap[i + j] : i + j);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
        }
        (*outputFunc)(outputStream, "end readonly def\n", 17);
        (*outputFunc)(outputStream, "FontName currentdict end definefont pop\n", 40);
    }

    // write the Type 0 parent font
    (*outputFunc)(outputStream, "16 dict begin\n", 14);
    (*outputFunc)(outputStream, "/FontName /", 11);
    (*outputFunc)(outputStream, psName, strlen(psName));
    (*outputFunc)(outputStream, " def\n", 5);
    (*outputFunc)(outputStream, "/FontType 0 def\n", 16);
    (*outputFunc)(outputStream, "/FontMatrix [1 0 0 1 0 0] def\n", 30);
    (*outputFunc)(outputStream, "/FMapType 2 def\n", 16);
    (*outputFunc)(outputStream, "/Encoding [\n", 12);
    for (i = 0; i < n; i += 256) {
        const std::string buf = GooString::format("{0:d}\n", i >> 8);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
    }
    (*outputFunc)(outputStream, "] def\n", 6);
    (*outputFunc)(outputStream, "/FDepVector [\n", 14);
    for (i = 0; i < n; i += 256) {
        (*outputFunc)(outputStream, "/", 1);
        (*outputFunc)(outputStream, psName, strlen(psName));
        const std::string buf = GooString::format("_{0:02x} findfont\n", i >> 8);
        (*outputFunc)(outputStream, buf.c_str(), buf.size());
    }
    (*outputFunc)(outputStream, "] def\n", 6);
    (*outputFunc)(outputStream, "FontName currentdict end definefont pop\n", 40);
}

void FoFiTrueType::convertToType0(const char *psName, int *cidMap, int nCIDs, FoFiOutputFunc outputFunc, void *outputStream) const
{
    char *start;
    int length;
    FoFiType1C *ff;

    if (!getCFFBlock(&start, &length)) {
        return;
    }
    if (!(ff = FoFiType1C::make((unsigned char *)start, length))) {
        return;
    }
    ff->convertToType0(psName, cidMap, nCIDs, outputFunc, outputStream);
    delete ff;
}

void FoFiTrueType::cvtEncoding(char **encoding, FoFiOutputFunc outputFunc, void *outputStream) const
{
    const char *name;
    int i;

    (*outputFunc)(outputStream, "/Encoding 256 array\n", 20);
    if (encoding) {
        for (i = 0; i < 256; ++i) {
            if (!(name = encoding[i])) {
                name = ".notdef";
            }
            const std::string buf = GooString::format("dup {0:d} /", i);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
            (*outputFunc)(outputStream, name, strlen(name));
            (*outputFunc)(outputStream, " put\n", 5);
        }
    } else {
        for (i = 0; i < 256; ++i) {
            const std::string buf = GooString::format("dup {0:d} /c{1:02x} put\n", i, i);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
        }
    }
    (*outputFunc)(outputStream, "readonly def\n", 13);
}

void FoFiTrueType::cvtCharStrings(char **encoding, const int *codeToGID, FoFiOutputFunc outputFunc, void *outputStream) const
{
    const char *name;
    char buf2[16];
    int i, k;

    // always define '.notdef'
    (*outputFunc)(outputStream, "/CharStrings 256 dict dup begin\n", 32);
    (*outputFunc)(outputStream, "/.notdef 0 def\n", 15);

    // if there's no 'cmap' table, punt
    if (cmaps.empty()) {
        goto err;
    }

    // map char name to glyph index:
    // 1. use encoding to map name to char code
    // 2. use codeToGID to map char code to glyph index
    // N.B. We do this in reverse order because font subsets can have
    //      weird encodings that use the same character name twice, and
    //      the first definition is probably the one we want.
    k = 0; // make gcc happy
    for (i = 255; i >= 0; --i) {
        if (encoding) {
            name = encoding[i];
        } else {
            sprintf(buf2, "c%02x", i);
            name = buf2;
        }
        if (name && strcmp(name, ".notdef")) {
            k = codeToGID[i];
            // note: Distiller (maybe Adobe's PS interpreter in general)
            // doesn't like TrueType fonts that have CharStrings entries
            // which point to nonexistent glyphs, hence the (k < nGlyphs)
            // test
            if (k > 0 && k < nGlyphs) {
                (*outputFunc)(outputStream, "/", 1);
                (*outputFunc)(outputStream, name, strlen(name));
                const std::string buf = GooString::format(" {0:d} def\n", k);
                (*outputFunc)(outputStream, buf.c_str(), buf.size());
            }
        }
    }

err:
    (*outputFunc)(outputStream, "end readonly def\n", 17);
}

void FoFiTrueType::cvtSfnts(FoFiOutputFunc outputFunc, void *outputStream, const GooString *name, bool needVerticalMetrics, int *maxUsedGlyph) const
{
    std::array<unsigned char, 54> headData;
    std::vector<unsigned char> locaData;
    TrueTypeTable newTables[nT42Tables];
    unsigned char tableDir[12 + nT42Tables * 16];
    bool ok;
    unsigned int checksum;
    int nNewTables;
    int glyfTableLen, length, glyfPos, j, k;
    std::array<unsigned char, 36> vheaTab = {
        0, 1, 0, 0, // table version number
        0, 0, // ascent
        0, 0, // descent
        0, 0, // reserved
        0, 0, // max advance height
        0, 0, // min top side bearing
        0, 0, // min bottom side bearing
        0, 0, // y max extent
        0, 0, // caret slope rise
        0, 1, // caret slope run
        0, 0, // caret offset
        0, 0, // reserved
        0, 0, // reserved
        0, 0, // reserved
        0, 0, // reserved
        0, 0, // metric data format
        0, 1 // number of advance heights in vmtx table
    };
    std::vector<unsigned char> vmtxTab;
    bool needVhea, needVmtx;
    int advance;

    *maxUsedGlyph = -1;

    // construct the 'head' table, zero out the font checksum
    int i = seekTable("head");
    if (i < 0 || i >= (int)tables.size()) {
        return;
    }
    int pos = tables[i].offset;
    if (!checkRegion(pos, 54)) {
        return;
    }
    memcpy(headData.data(), file + pos, 54);
    headData[8] = headData[9] = headData[10] = headData[11] = (unsigned char)0;

    // check for a bogus loca format field in the 'head' table
    // (I've encountered fonts with loca format set to 0x0100 instead of 0x0001)
    if (locaFmt != 0 && locaFmt != 1) {
        headData[50] = 0;
        headData[51] = 1;
    }

    // read the original 'loca' table, pad entries out to 4 bytes, and
    // sort it into proper order -- some (non-compliant) fonts have
    // out-of-order loca tables; in order to correctly handle the case
    // where (compliant) fonts have empty entries in the middle of the
    // table, cmpTrueTypeLocaOffset uses offset as its primary sort key,
    // and idx as its secondary key (ensuring that adjacent entries with
    // the same pos value remain in the same order)
    std::vector<TrueTypeLoca> locaTable { size_t(nGlyphs + 1) };
    i = seekTable("loca");
    pos = tables[i].offset;
    i = seekTable("glyf");
    glyfTableLen = tables[i].len;
    ok = true;
    for (i = 0; i <= nGlyphs; ++i) {
        locaTable[i].idx = i;
        if (locaFmt) {
            locaTable[i].origOffset = (int)getU32BE(pos + i * 4, &ok);
        } else {
            locaTable[i].origOffset = 2 * getU16BE(pos + i * 2, &ok);
        }
        if (locaTable[i].origOffset > glyfTableLen) {
            locaTable[i].origOffset = glyfTableLen;
        }
    }
    std::sort(locaTable.begin(), locaTable.end(), cmpTrueTypeLocaOffsetFunctor());
    for (i = 0; i < nGlyphs; ++i) {
        locaTable[i].len = locaTable[i + 1].origOffset - locaTable[i].origOffset;
    }
    locaTable[nGlyphs].len = 0;
    std::sort(locaTable.begin(), locaTable.end(), cmpTrueTypeLocaIdxFunctor());
    pos = 0;
    for (i = 0; i <= nGlyphs; ++i) {
        locaTable[i].newOffset = pos;

        int newPos;
        if (unlikely(checkedAdd(pos, locaTable[i].len, &newPos))) {
            ok = false;
        } else {
            pos = newPos;
            if (pos & 3) {
                pos += 4 - (pos & 3);
            }
        }
        if (locaTable[i].len > 0) {
            *maxUsedGlyph = i;
        }
    }

    // construct the new 'loca' table
    locaData.resize((nGlyphs + 1) * (locaFmt ? 4 : 2));
    for (i = 0; i <= nGlyphs; ++i) {
        pos = locaTable[i].newOffset;
        if (locaFmt) {
            locaData[4 * i] = (unsigned char)(pos >> 24);
            locaData[4 * i + 1] = (unsigned char)(pos >> 16);
            locaData[4 * i + 2] = (unsigned char)(pos >> 8);
            locaData[4 * i + 3] = (unsigned char)pos;
        } else {
            locaData[2 * i] = (unsigned char)(pos >> 9);
            locaData[2 * i + 1] = (unsigned char)(pos >> 1);
        }
    }

    // count the number of tables
    nNewTables = 0;
    for (i = 0; i < nT42Tables; ++i) {
        if (t42Tables[i].required || seekTable(t42Tables[i].tag) >= 0) {
            ++nNewTables;
        }
    }
    advance = 0; // make gcc happy
    if (needVerticalMetrics) {
        needVhea = seekTable("vhea") < 0;
        needVmtx = seekTable("vmtx") < 0;
        if (needVhea || needVmtx) {
            i = seekTable("head");
            advance = getU16BE(tables[i].offset + 18, &ok); // units per em
            if (needVhea) {
                ++nNewTables;
            }
            if (needVmtx) {
                ++nNewTables;
            }
        }
    }

    // construct the new table headers, including table checksums
    // (pad each table out to a multiple of 4 bytes)
    pos = 12 + nNewTables * 16;
    k = 0;
    for (i = 0; i < nT42Tables; ++i) {
        length = -1;
        checksum = 0; // make gcc happy
        if (i == t42HeadTable) {
            length = 54;
            checksum = computeTableChecksum(headData);
        } else if (i == t42LocaTable) {
            length = (nGlyphs + 1) * (locaFmt ? 4 : 2);
            checksum = computeTableChecksum(locaData);
        } else if (i == t42GlyfTable) {
            length = 0;
            checksum = 0;
            glyfPos = tables[seekTable("glyf")].offset;
            for (j = 0; j < nGlyphs; ++j) {
                length += locaTable[j].len;
                if (length & 3) {
                    length += 4 - (length & 3);
                }
                if (checkRegion(glyfPos + locaTable[j].origOffset, locaTable[j].len)) {
                    checksum += computeTableChecksum(std::span(file + glyfPos + locaTable[j].origOffset, locaTable[j].len));
                }
            }
        } else {
            if ((j = seekTable(t42Tables[i].tag)) >= 0) {
                length = tables[j].len;
                if (checkRegion(tables[j].offset, length)) {
                    checksum = computeTableChecksum(std::span(file + tables[j].offset, length));
                }
            } else if (needVerticalMetrics && i == t42VheaTable) {
                vheaTab[10] = advance / 256; // max advance height
                vheaTab[11] = advance % 256;
                length = vheaTab.size();
                checksum = computeTableChecksum(vheaTab);
            } else if (needVerticalMetrics && i == t42VmtxTable) {
                length = 4 + (nGlyphs - 1) * 2;
                vmtxTab.resize(length, 0);
                vmtxTab[0] = advance / 256;
                vmtxTab[1] = advance % 256;
                checksum = computeTableChecksum(vmtxTab);
            } else if (t42Tables[i].required) {
                //~ error(-1, "Embedded TrueType font is missing a required table ('%s')",
                //~       t42Tables[i].tag);
                length = 0;
                checksum = 0;
            }
        }
        if (length >= 0) {
            newTables[k].tag = ((t42Tables[i].tag[0] & 0xff) << 24) | ((t42Tables[i].tag[1] & 0xff) << 16) | ((t42Tables[i].tag[2] & 0xff) << 8) | (t42Tables[i].tag[3] & 0xff);
            newTables[k].checksum = checksum;
            newTables[k].offset = pos;
            newTables[k].len = length;
            int newPos;
            if (unlikely(checkedAdd(pos, length, &newPos))) {
                ok = false;
            } else {
                pos = newPos;
                if (pos & 3) {
                    pos += 4 - (length & 3);
                }
            }
            ++k;
        }
    }
    if (unlikely(k < nNewTables)) {
        error(errSyntaxWarning, -1, "unexpected number of tables");
        nNewTables = k;
    }

    // construct the table directory
    tableDir[0] = 0x00; // sfnt version
    tableDir[1] = 0x01;
    tableDir[2] = 0x00;
    tableDir[3] = 0x00;
    tableDir[4] = 0; // numTables
    tableDir[5] = nNewTables;
    tableDir[6] = 0; // searchRange
    tableDir[7] = (unsigned char)128;
    tableDir[8] = 0; // entrySelector
    tableDir[9] = 3;
    tableDir[10] = 0; // rangeShift
    tableDir[11] = (unsigned char)(16 * nNewTables - 128);
    pos = 12;
    for (i = 0; i < nNewTables; ++i) {
        tableDir[pos] = (unsigned char)(newTables[i].tag >> 24);
        tableDir[pos + 1] = (unsigned char)(newTables[i].tag >> 16);
        tableDir[pos + 2] = (unsigned char)(newTables[i].tag >> 8);
        tableDir[pos + 3] = (unsigned char)newTables[i].tag;
        tableDir[pos + 4] = (unsigned char)(newTables[i].checksum >> 24);
        tableDir[pos + 5] = (unsigned char)(newTables[i].checksum >> 16);
        tableDir[pos + 6] = (unsigned char)(newTables[i].checksum >> 8);
        tableDir[pos + 7] = (unsigned char)newTables[i].checksum;
        tableDir[pos + 8] = (unsigned char)(newTables[i].offset >> 24);
        tableDir[pos + 9] = (unsigned char)(newTables[i].offset >> 16);
        tableDir[pos + 10] = (unsigned char)(newTables[i].offset >> 8);
        tableDir[pos + 11] = (unsigned char)newTables[i].offset;
        tableDir[pos + 12] = (unsigned char)(newTables[i].len >> 24);
        tableDir[pos + 13] = (unsigned char)(newTables[i].len >> 16);
        tableDir[pos + 14] = (unsigned char)(newTables[i].len >> 8);
        tableDir[pos + 15] = (unsigned char)newTables[i].len;
        pos += 16;
    }

    // compute the font checksum and store it in the head table
    checksum = computeTableChecksum(std::span(tableDir, 12 + nNewTables * 16));
    for (i = 0; i < nNewTables; ++i) {
        checksum += newTables[i].checksum;
    }
    checksum = 0xb1b0afba - checksum; // because the TrueType spec says so
    headData[8] = (unsigned char)(checksum >> 24);
    headData[9] = (unsigned char)(checksum >> 16);
    headData[10] = (unsigned char)(checksum >> 8);
    headData[11] = (unsigned char)checksum;

    // start the sfnts array
    if (name) {
        (*outputFunc)(outputStream, "/", 1);
        (*outputFunc)(outputStream, name->c_str(), name->getLength());
        (*outputFunc)(outputStream, " [\n", 3);
    } else {
        (*outputFunc)(outputStream, "/sfnts [\n", 9);
    }

    // write the table directory
    dumpString(std::span(tableDir, 12 + nNewTables * 16), outputFunc, outputStream);

    // write the tables
    for (i = 0; i < nNewTables; ++i) {
        if (i == t42HeadTable) {
            dumpString(headData, outputFunc, outputStream);
        } else if (i == t42LocaTable) {
            length = (nGlyphs + 1) * (locaFmt ? 4 : 2);
            dumpString(locaData, outputFunc, outputStream);
        } else if (i == t42GlyfTable) {
            glyfPos = tables[seekTable("glyf")].offset;
            for (j = 0; j < nGlyphs; ++j) {
                if (locaTable[j].len > 0 && checkRegion(glyfPos + locaTable[j].origOffset, locaTable[j].len)) {
                    dumpString(std::span(file + glyfPos + locaTable[j].origOffset, locaTable[j].len), outputFunc, outputStream);
                }
            }
        } else {
            // length == 0 means the table is missing and the error was
            // already reported during the construction of the table
            // headers
            if ((length = newTables[i].len) > 0) {
                if ((j = seekTable(t42Tables[i].tag)) >= 0 && checkRegion(tables[j].offset, tables[j].len)) {
                    dumpString(std::span(file + tables[j].offset, tables[j].len), outputFunc, outputStream);
                } else if (needVerticalMetrics && i == t42VheaTable) {
                    if (unlikely(length > (int)sizeof(vheaTab))) {
                        error(errSyntaxWarning, -1, "length bigger than vheaTab size");
                        length = sizeof(vheaTab);
                    }
                    dumpString(vheaTab, outputFunc, outputStream);
                } else if (needVerticalMetrics && i == t42VmtxTable) {
                    if (unlikely(length > (int)vmtxTab.size())) {
                        error(errSyntaxWarning, -1, "length bigger than vmtxTab size");
                    }
                    dumpString(vmtxTab, outputFunc, outputStream);
                }
            }
        }
    }

    // end the sfnts array
    (*outputFunc)(outputStream, "] def\n", 6);
}

void FoFiTrueType::dumpString(std::span<const unsigned char> s, FoFiOutputFunc outputFunc, void *outputStream)
{
    (*outputFunc)(outputStream, "<", 1);
    for (int i = 0; i < (int)s.size(); i += 32) {
        for (int j = 0; j < 32 && i + j < (int)s.size(); ++j) {
            const std::string buf = GooString::format("{0:02x}", s[i + j] & 0xff);
            (*outputFunc)(outputStream, buf.c_str(), buf.size());
        }
        if (i % (65536 - 32) == 65536 - 64) {
            (*outputFunc)(outputStream, ">\n<", 3);
        } else if (i + 32 < (int)s.size()) {
            (*outputFunc)(outputStream, "\n", 1);
        }
    }
    if (s.size() & 3) {
        int pad = 4 - (s.size() & 3);
        for (int i = 0; i < pad; ++i) {
            (*outputFunc)(outputStream, "00", 2);
        }
    }
    // add an extra zero byte because the Adobe Type 42 spec says so
    (*outputFunc)(outputStream, "00>\n", 4);
}

unsigned int FoFiTrueType::computeTableChecksum(std::span<const unsigned char> data)
{
    unsigned checksum = 0;
    for (size_t i = 0; i + 3 < data.size(); i += 4) {
        unsigned int word = ((data[i] & 0xff) << 24) + ((data[i + 1] & 0xff) << 16) + ((data[i + 2] & 0xff) << 8) + (data[i + 3] & 0xff);
        checksum += word;
    }
    if (data.size() & 3) {
        unsigned int word = 0;
        int i = data.size() & ~3;
        switch (data.size() & 3) {
        case 3:
            word |= (data[i + 2] & 0xff) << 8;
            // fallthrough
        case 2:
            word |= (data[i + 1] & 0xff) << 16;
            // fallthrough
        case 1:
            word |= (data[i] & 0xff) << 24;
            break;
        }
        checksum += word;
    }
    return checksum;
}

void FoFiTrueType::parse()
{
    unsigned int topTag;
    int pos, ver, i, j;

    parsedOk = true;

    // look for a collection (TTC)
    topTag = getU32BE(0, &parsedOk);
    if (!parsedOk) {
        return;
    }
    if (topTag == ttcfTag) {
        /* TTC font */
        int dircount;

        dircount = getU32BE(8, &parsedOk);
        if (!parsedOk) {
            return;
        }
        if (!dircount) {
            parsedOk = false;
            return;
        }

        if (faceIndex >= dircount) {
            faceIndex = 0;
        }
        pos = getU32BE(12 + faceIndex * 4, &parsedOk);
        if (!parsedOk) {
            return;
        }
    } else {
        pos = 0;
    }

    // check the sfnt version
    ver = getU32BE(pos, &parsedOk);
    if (!parsedOk) {
        return;
    }
    openTypeCFF = ver == 0x4f54544f; // 'OTTO'

    // read the table directory
    int nTables = getU16BE(pos + 4, &parsedOk);
    if (!parsedOk) {
        return;
    }
    tables.resize(nTables);
    pos += 12;
    j = 0;
    for (i = 0; i < nTables; ++i) {
        tables[j].tag = getU32BE(pos, &parsedOk);
        tables[j].checksum = getU32BE(pos + 4, &parsedOk);
        tables[j].offset = (int)getU32BE(pos + 8, &parsedOk);
        tables[j].len = (int)getU32BE(pos + 12, &parsedOk);
        if (unlikely((tables[j].offset < 0) || (tables[j].len < 0) || (tables[j].offset < INT_MAX - tables[j].len) || (tables[j].len > INT_MAX - tables[j].offset)
                     || (tables[j].offset + tables[j].len >= tables[j].offset && tables[j].offset + tables[j].len <= len))) {
            // ignore any bogus entries in the table directory
            ++j;
        }
        pos += 16;
    }
    if (nTables != j) {
        nTables = j;
        tables.resize(nTables);
    }
    if (!parsedOk || tables.empty()) {
        parsedOk = false;
        return;
    }

    // check for tables that are required by both the TrueType spec and
    // the Type 42 spec
    if (seekTable("head") < 0 || seekTable("hhea") < 0 || seekTable("maxp") < 0 || (!openTypeCFF && seekTable("loca") < 0) || (!openTypeCFF && seekTable("glyf") < 0) || (openTypeCFF && (seekTable("CFF ") < 0 && seekTable("CFF2") < 0))) {
        parsedOk = false;
        return;
    }

    // read the cmaps
    if ((i = seekTable("cmap")) >= 0) {
        pos = tables[i].offset + 2;
        int nCmaps = getU16BE(pos, &parsedOk);
        pos += 2;
        if (!parsedOk) {
            return;
        }
        cmaps.resize(nCmaps);
        for (j = 0; j < nCmaps; ++j) {
            cmaps[j].platform = getU16BE(pos, &parsedOk);
            cmaps[j].encoding = getU16BE(pos + 2, &parsedOk);
            cmaps[j].offset = tables[i].offset + getU32BE(pos + 4, &parsedOk);
            pos += 8;
            cmaps[j].fmt = getU16BE(cmaps[j].offset, &parsedOk);
            cmaps[j].len = getU16BE(cmaps[j].offset + 2, &parsedOk);
        }
        if (!parsedOk) {
            cmaps.clear();
            return;
        }
    } else {
        cmaps.clear();
    }

    // get the number of glyphs from the maxp table
    i = seekTable("maxp");
    nGlyphs = getU16BE(tables[i].offset + 4, &parsedOk);
    if (!parsedOk) {
        return;
    }

    // get the bbox and loca table format from the head table
    i = seekTable("head");
    bbox[0] = getS16BE(tables[i].offset + 36, &parsedOk);
    bbox[1] = getS16BE(tables[i].offset + 38, &parsedOk);
    bbox[2] = getS16BE(tables[i].offset + 40, &parsedOk);
    bbox[3] = getS16BE(tables[i].offset + 42, &parsedOk);
    locaFmt = getS16BE(tables[i].offset + 50, &parsedOk);
    if (!parsedOk) {
        return;
    }

    // read the post table
    readPostTable();
}

void FoFiTrueType::readPostTable()
{
    std::string name;
    int tablePos, postFmt, stringIdx, stringPos;
    bool ok;
    int i, j, n, m;

    ok = true;
    if ((i = seekTable("post")) < 0) {
        return;
    }
    tablePos = tables[i].offset;
    postFmt = getU32BE(tablePos, &ok);
    if (!ok) {
        goto err;
    }
    if (postFmt == 0x00010000) {
        nameToGID.reserve(258);
        for (i = 0; i < 258; ++i) {
            nameToGID.emplace(macGlyphNames[i], i);
        }
    } else if (postFmt == 0x00020000) {
        nameToGID.reserve(258);
        n = getU16BE(tablePos + 32, &ok);
        if (!ok) {
            goto err;
        }
        if (n > nGlyphs) {
            n = nGlyphs;
        }
        stringIdx = 0;
        stringPos = tablePos + 34 + 2 * n;
        for (i = 0; i < n; ++i) {
            ok = true;
            j = getU16BE(tablePos + 34 + 2 * i, &ok);
            if (j < 258) {
                nameToGID[macGlyphNames[j]] = i;
            } else {
                j -= 258;
                if (j != stringIdx) {
                    for (stringIdx = 0, stringPos = tablePos + 34 + 2 * n; stringIdx < j; ++stringIdx, stringPos += 1 + getU8(stringPos, &ok)) {
                        ;
                    }
                    if (!ok) {
                        continue;
                    }
                }
                m = getU8(stringPos, &ok);
                if (!ok || !checkRegion(stringPos + 1, m)) {
                    continue;
                }
                name.assign((char *)&file[stringPos + 1], m);
                nameToGID[name] = i;
                ++stringIdx;
                stringPos += 1 + m;
            }
        }
    } else if (postFmt == 0x00028000) {
        nameToGID.reserve(258);
        for (i = 0; i < nGlyphs; ++i) {
            j = getU8(tablePos + 32 + i, &ok);
            if (!ok) {
                continue;
            }
            if (j < 258) {
                nameToGID[macGlyphNames[j]] = i;
            }
        }
    }

    return;

err:
    nameToGID.clear();
}

int FoFiTrueType::seekTable(const char *tag) const
{
    unsigned int tagI;

    tagI = ((tag[0] & 0xff) << 24) | ((tag[1] & 0xff) << 16) | ((tag[2] & 0xff) << 8) | (tag[3] & 0xff);
    for (int i = 0; i < (int)tables.size(); ++i) {
        if (tables[i].tag == tagI) {
            return i;
        }
    }
    return -1;
}

unsigned int FoFiTrueType::charToTag(const char *tagName)
{
    int n = strlen(tagName);
    unsigned int tag = 0;
    int i;

    if (n > 4) {
        n = 4;
    }
    for (i = 0; i < n; i++) {
        tag <<= 8;
        tag |= tagName[i] & 0xff;
    }
    for (; i < 4; i++) {
        tag <<= 8;
        tag |= ' ';
    }
    return tag;
}

/*
  setup GSUB table data
  Only supporting vertical text substitution.
*/
int FoFiTrueType::setupGSUB(const char *scriptName)
{
    return setupGSUB(scriptName, nullptr);
}

/*
  setup GSUB table data
  Only supporting vertical text substitution.
*/
int FoFiTrueType::setupGSUB(const char *scriptName, const char *languageName)
{
    unsigned int gsubTable;
    unsigned int i;
    unsigned int scriptList, featureList;
    unsigned int scriptCount;
    unsigned int tag;
    unsigned int scriptTable = 0;
    unsigned int langSys;
    unsigned int featureCount;
    unsigned int featureIndex;
    unsigned int ftable = 0;
    unsigned int llist;
    unsigned int scriptTag;
    int x;
    unsigned int pos;

    if (scriptName == nullptr) {
        gsubFeatureTable = 0;
        return 0;
    }
    scriptTag = charToTag(scriptName);
    /* read GSUB Header */
    if ((x = seekTable("GSUB")) < 0) {
        return 0; /* GSUB table not found */
    }
    gsubTable = tables[x].offset;
    pos = gsubTable + 4;
    scriptList = getU16BE(pos, &parsedOk);
    pos += 2;
    featureList = getU16BE(pos, &parsedOk);
    pos += 2;
    llist = getU16BE(pos, &parsedOk);

    gsubLookupList = llist + gsubTable; /* change to offset from top of file */
    /* read script list table */
    pos = gsubTable + scriptList;
    scriptCount = getU16BE(pos, &parsedOk);
    pos += 2;
    /* find  script */
    for (i = 0; i < scriptCount; i++) {
        tag = getU32BE(pos, &parsedOk);
        pos += 4;
        scriptTable = getU16BE(pos, &parsedOk);
        pos += 2;
        if (tag == scriptTag) {
            /* found */
            break;
        }
    }
    if (i >= scriptCount) {
        /* not found */
        return 0;
    }

    /* read script table */
    /* use default language system */
    pos = gsubTable + scriptList + scriptTable;
    langSys = 0;
    if (languageName) {
        unsigned int langTag = charToTag(languageName);
        unsigned int langCount = getU16BE(pos + 2, &parsedOk);
        for (i = 0; i < langCount && langSys == 0; i++) {
            tag = getU32BE(pos + 4 + i * (4 + 2), &parsedOk);
            if (tag == langTag) {
                langSys = getU16BE(pos + 4 + i * (4 + 2) + 4, &parsedOk);
            }
        }
    }
    if (langSys == 0) {
        /* default language system */
        langSys = getU16BE(pos, &parsedOk);
    }

    /* read LangSys table */
    if (langSys == 0) {
        /* no default LangSys */
        return 0;
    }

    pos = gsubTable + scriptList + scriptTable + langSys + 2;
    featureIndex = getU16BE(pos, &parsedOk); /* ReqFeatureIndex */
    pos += 2;

    if (featureIndex != 0xffff) {
        unsigned int tpos;
        /* read feature record */
        tpos = gsubTable + featureList;
        featureCount = getU16BE(tpos, &parsedOk);
        tpos = gsubTable + featureList + 2 + featureIndex * (4 + 2);
        tag = getU32BE(tpos, &parsedOk);
        tpos += 4;
        if (tag == vrt2Tag) {
            /* vrt2 is preferred, overwrite vert */
            ftable = getU16BE(tpos, &parsedOk);
            /* convert to offset from file top */
            gsubFeatureTable = ftable + gsubTable + featureList;
            return 0;
        } else if (tag == vertTag) {
            ftable = getU16BE(tpos, &parsedOk);
        }
    }
    featureCount = getU16BE(pos, &parsedOk);
    pos += 2;
    /* find 'vrt2' or 'vert' feature */
    for (i = 0; i < featureCount; i++) {
        unsigned int oldPos;

        featureIndex = getU16BE(pos, &parsedOk);
        pos += 2;
        oldPos = pos; /* save position */
        /* read feature record */
        pos = gsubTable + featureList + 2 + featureIndex * (4 + 2);
        tag = getU32BE(pos, &parsedOk);
        pos += 4;
        if (tag == vrt2Tag) {
            /* vrt2 is preferred, overwrite vert */
            ftable = getU16BE(pos, &parsedOk);
            break;
        } else if (ftable == 0 && tag == vertTag) {
            ftable = getU16BE(pos, &parsedOk);
        }
        pos = oldPos; /* restore old position */
    }
    if (ftable == 0) {
        /* vert nor vrt2 are not found */
        return 0;
    }
    /* convert to offset from file top */
    gsubFeatureTable = ftable + gsubTable + featureList;
    return 0;
}

unsigned int FoFiTrueType::doMapToVertGID(unsigned int orgGID)
{
    unsigned int lookupCount;
    unsigned int lookupListIndex;
    unsigned int i;
    unsigned int gid = 0;
    unsigned int pos;

    pos = gsubFeatureTable + 2;
    lookupCount = getU16BE(pos, &parsedOk);
    pos += 2;
    for (i = 0; i < lookupCount; i++) {
        lookupListIndex = getU16BE(pos, &parsedOk);
        pos += 2;
        if ((gid = scanLookupList(lookupListIndex, orgGID)) != 0) {
            break;
        }
    }
    return gid;
}

unsigned int FoFiTrueType::mapToVertGID(unsigned int orgGID)
{
    unsigned int mapped;

    if (gsubFeatureTable == 0) {
        return orgGID;
    }
    if ((mapped = doMapToVertGID(orgGID)) != 0) {
        return mapped;
    }
    return orgGID;
}

unsigned int FoFiTrueType::scanLookupList(unsigned int listIndex, unsigned int orgGID)
{
    unsigned int lookupTable;
    unsigned int subTableCount;
    unsigned int subTable;
    unsigned int i;
    unsigned int gid = 0;
    unsigned int pos;

    if (gsubLookupList == 0) {
        return 0; /* no lookup list */
    }
    pos = gsubLookupList + 2 + listIndex * 2;
    lookupTable = getU16BE(pos, &parsedOk);
    /* read lookup table */
    pos = gsubLookupList + lookupTable + 4;
    subTableCount = getU16BE(pos, &parsedOk);
    pos += 2;
    ;
    for (i = 0; i < subTableCount; i++) {
        subTable = getU16BE(pos, &parsedOk);
        pos += 2;
        if ((gid = scanLookupSubTable(gsubLookupList + lookupTable + subTable, orgGID)) != 0) {
            break;
        }
    }
    return gid;
}

unsigned int FoFiTrueType::scanLookupSubTable(unsigned int subTable, unsigned int orgGID)
{
    unsigned int format;
    unsigned int coverage;
    int delta;
    int glyphCount;
    unsigned int substitute;
    unsigned int gid = 0;
    int coverageIndex;
    int pos;

    pos = subTable;
    format = getU16BE(pos, &parsedOk);
    pos += 2;
    coverage = getU16BE(pos, &parsedOk);
    pos += 2;
    if ((coverageIndex = checkGIDInCoverage(subTable + coverage, orgGID)) >= 0) {
        switch (format) {
        case 1:
            /* format 1 */
            delta = getS16BE(pos, &parsedOk);
            pos += 2;
            gid = orgGID + delta;
            break;
        case 2:
            /* format 2 */
            glyphCount = getS16BE(pos, &parsedOk);
            pos += 2;
            if (glyphCount > coverageIndex) {
                pos += coverageIndex * 2;
                substitute = getU16BE(pos, &parsedOk);
                gid = substitute;
            }
            break;
        default:
            /* unknown format */
            break;
        }
    }
    return gid;
}

int FoFiTrueType::checkGIDInCoverage(unsigned int coverage, unsigned int orgGID)
{
    int index = -1;
    unsigned int format;
    unsigned int count;
    unsigned int i;
    unsigned int pos;

    pos = coverage;
    format = getU16BE(pos, &parsedOk);
    pos += 2;
    switch (format) {
    case 1:
        count = getU16BE(pos, &parsedOk);
        pos += 2;
        // In some poor CJK fonts, key GIDs are not sorted,
        // thus we cannot finish checking even when the range
        // including orgGID seems to have already passed.
        for (i = 0; i < count; i++) {
            unsigned int gid;

            gid = getU16BE(pos, &parsedOk);
            pos += 2;
            if (gid == orgGID) {
                /* found */
                index = i;
                break;
            }
        }
        break;
    case 2:
        count = getU16BE(pos, &parsedOk);
        pos += 2;
        for (i = 0; i < count; i++) {
            unsigned int startGID, endGID;
            unsigned int startIndex;

            startGID = getU16BE(pos, &parsedOk);
            pos += 2;
            endGID = getU16BE(pos, &parsedOk);
            pos += 2;
            startIndex = getU16BE(pos, &parsedOk);
            pos += 2;
            // In some poor CJK fonts, key GIDs are not sorted,
            // thus we cannot finish checking even when the range
            // including orgGID seems to have already passed.
            if (startGID <= orgGID && orgGID <= endGID) {
                /* found */
                index = startIndex + orgGID - startGID;
                break;
            }
        }
        break;
    default:
        break;
    }
    return index;
}
