checkAndRepair()

in src/core/fonts.js [1313:3114]


  checkAndRepair(name, font, properties) {
    const VALID_TABLES = [
      "OS/2",
      "cmap",
      "head",
      "hhea",
      "hmtx",
      "maxp",
      "name",
      "post",
      "loca",
      "glyf",
      "fpgm",
      "prep",
      "cvt ",
      "CFF ",
    ];

    function readTables(file, numTables) {
      const tables = Object.create(null);
      tables["OS/2"] = null;
      tables.cmap = null;
      tables.head = null;
      tables.hhea = null;
      tables.hmtx = null;
      tables.maxp = null;
      tables.name = null;
      tables.post = null;

      for (let i = 0; i < numTables; i++) {
        const table = readTableEntry(file);
        if (!VALID_TABLES.includes(table.tag)) {
          continue; // skipping table if it's not a required or optional table
        }
        if (table.length === 0) {
          continue; // skipping empty tables
        }
        tables[table.tag] = table;
      }
      return tables;
    }

    function readTableEntry(file) {
      const tag = file.getString(4);

      const checksum = file.getInt32() >>> 0;
      const offset = file.getInt32() >>> 0;
      const length = file.getInt32() >>> 0;

      // Read the table associated data
      const previousPosition = file.pos;
      file.pos = file.start || 0;
      file.skip(offset);
      const data = file.getBytes(length);
      file.pos = previousPosition;

      if (tag === "head") {
        // clearing checksum adjustment
        data[8] = data[9] = data[10] = data[11] = 0;
        data[17] |= 0x20; // Set font optimized for cleartype flag.
      }

      return {
        tag,
        checksum,
        length,
        offset,
        data,
      };
    }

    function readOpenTypeHeader(ttf) {
      return {
        version: ttf.getString(4),
        numTables: ttf.getUint16(),
        searchRange: ttf.getUint16(),
        entrySelector: ttf.getUint16(),
        rangeShift: ttf.getUint16(),
      };
    }

    function readTrueTypeCollectionHeader(ttc) {
      const ttcTag = ttc.getString(4);
      assert(ttcTag === "ttcf", "Must be a TrueType Collection font.");

      const majorVersion = ttc.getUint16();
      const minorVersion = ttc.getUint16();
      const numFonts = ttc.getInt32() >>> 0;
      const offsetTable = [];
      for (let i = 0; i < numFonts; i++) {
        offsetTable.push(ttc.getInt32() >>> 0);
      }

      const header = {
        ttcTag,
        majorVersion,
        minorVersion,
        numFonts,
        offsetTable,
      };
      switch (majorVersion) {
        case 1:
          return header;
        case 2:
          header.dsigTag = ttc.getInt32() >>> 0;
          header.dsigLength = ttc.getInt32() >>> 0;
          header.dsigOffset = ttc.getInt32() >>> 0;
          return header;
      }
      throw new FormatError(
        `Invalid TrueType Collection majorVersion: ${majorVersion}.`
      );
    }

    function readTrueTypeCollectionData(ttc, fontName) {
      const { numFonts, offsetTable } = readTrueTypeCollectionHeader(ttc);
      const fontNameParts = fontName.split("+");
      let fallbackData;

      for (let i = 0; i < numFonts; i++) {
        ttc.pos = (ttc.start || 0) + offsetTable[i];
        const potentialHeader = readOpenTypeHeader(ttc);
        const potentialTables = readTables(ttc, potentialHeader.numTables);

        if (!potentialTables.name) {
          throw new FormatError(
            'TrueType Collection font must contain a "name" table.'
          );
        }
        const [nameTable] = readNameTable(potentialTables.name);

        for (let j = 0, jj = nameTable.length; j < jj; j++) {
          for (let k = 0, kk = nameTable[j].length; k < kk; k++) {
            const nameEntry = nameTable[j][k]?.replaceAll(/\s/g, "");
            if (!nameEntry) {
              continue;
            }
            if (nameEntry === fontName) {
              return {
                header: potentialHeader,
                tables: potentialTables,
              };
            }
            if (fontNameParts.length < 2) {
              continue;
            }
            for (const part of fontNameParts) {
              if (nameEntry === part) {
                fallbackData = {
                  name: part,
                  header: potentialHeader,
                  tables: potentialTables,
                };
              }
            }
          }
        }
      }
      if (fallbackData) {
        warn(
          `TrueType Collection does not contain "${fontName}" font, ` +
            `falling back to "${fallbackData.name}" font instead.`
        );
        return {
          header: fallbackData.header,
          tables: fallbackData.tables,
        };
      }
      throw new FormatError(
        `TrueType Collection does not contain "${fontName}" font.`
      );
    }

    /**
     * Read the appropriate subtable from the cmap according to 9.6.6.4 from
     * PDF spec
     */
    function readCmapTable(cmap, file, isSymbolicFont, hasEncoding) {
      if (!cmap) {
        warn("No cmap table available.");
        return {
          platformId: -1,
          encodingId: -1,
          mappings: [],
          hasShortCmap: false,
        };
      }
      let segment;
      let start = (file.start || 0) + cmap.offset;
      file.pos = start;

      file.skip(2); // version
      const numTables = file.getUint16();

      let potentialTable;
      let canBreak = false;
      // There's an order of preference in terms of which cmap subtable to
      // use:
      // - non-symbolic fonts the preference is a 3,1 table then a 1,0 table
      // - symbolic fonts the preference is a 3,0 table then a 1,0 table
      // The following takes advantage of the fact that the tables are sorted
      // to work.
      for (let i = 0; i < numTables; i++) {
        const platformId = file.getUint16();
        const encodingId = file.getUint16();
        const offset = file.getInt32() >>> 0;
        let useTable = false;

        // Sometimes there are multiple of the same type of table. Default
        // to choosing the first table and skip the rest.
        if (
          potentialTable?.platformId === platformId &&
          potentialTable?.encodingId === encodingId
        ) {
          continue;
        }

        if (
          platformId === 0 &&
          (encodingId === /* Unicode Default */ 0 ||
            encodingId === /* Unicode 1.1 */ 1 ||
            encodingId === /* Unicode BMP */ 3)
        ) {
          useTable = true;
          // Continue the loop since there still may be a higher priority
          // table.
        } else if (platformId === 1 && encodingId === 0) {
          useTable = true;
          // Continue the loop since there still may be a higher priority
          // table.
        } else if (
          platformId === 3 &&
          encodingId === 1 &&
          (hasEncoding || !potentialTable)
        ) {
          useTable = true;
          if (!isSymbolicFont) {
            canBreak = true;
          }
        } else if (isSymbolicFont && platformId === 3 && encodingId === 0) {
          useTable = true;

          let correctlySorted = true;
          if (i < numTables - 1) {
            const nextBytes = file.peekBytes(2),
              nextPlatformId = int16(nextBytes[0], nextBytes[1]);
            if (nextPlatformId < platformId) {
              correctlySorted = false;
            }
          }
          if (correctlySorted) {
            canBreak = true;
          }
        }

        if (useTable) {
          potentialTable = {
            platformId,
            encodingId,
            offset,
          };
        }
        if (canBreak) {
          break;
        }
      }

      if (potentialTable) {
        file.pos = start + potentialTable.offset;
      }
      if (!potentialTable || file.peekByte() === -1) {
        warn("Could not find a preferred cmap table.");
        return {
          platformId: -1,
          encodingId: -1,
          mappings: [],
          hasShortCmap: false,
        };
      }

      const format = file.getUint16();
      let hasShortCmap = false;
      const mappings = [];
      let j, glyphId;

      // TODO(mack): refactor this cmap subtable reading logic out
      if (format === 0) {
        file.skip(2 + 2); // length + language

        for (j = 0; j < 256; j++) {
          const index = file.getByte();
          if (!index) {
            continue;
          }
          mappings.push({
            charCode: j,
            glyphId: index,
          });
        }
        hasShortCmap = true;
      } else if (format === 2) {
        file.skip(2 + 2); // length + language

        const subHeaderKeys = [];
        let maxSubHeaderKey = 0;
        // Read subHeaderKeys. If subHeaderKeys[i] === 0, then i is a
        // single-byte character. Otherwise, i is the first byte of a
        // multi-byte character, and the value is 8*index into
        // subHeaders.
        for (let i = 0; i < 256; i++) {
          const subHeaderKey = file.getUint16() >> 3;
          subHeaderKeys.push(subHeaderKey);
          maxSubHeaderKey = Math.max(subHeaderKey, maxSubHeaderKey);
        }
        // Read subHeaders. The number of entries is determined
        // dynamically based on the subHeaderKeys found above.
        const subHeaders = [];
        for (let i = 0; i <= maxSubHeaderKey; i++) {
          subHeaders.push({
            firstCode: file.getUint16(),
            entryCount: file.getUint16(),
            idDelta: signedInt16(file.getByte(), file.getByte()),
            idRangePos: file.pos + file.getUint16(),
          });
        }
        for (let i = 0; i < 256; i++) {
          if (subHeaderKeys[i] === 0) {
            // i is a single-byte code.
            file.pos = subHeaders[0].idRangePos + 2 * i;
            glyphId = file.getUint16();
            mappings.push({
              charCode: i,
              glyphId,
            });
          } else {
            // i is the first byte of a two-byte code.
            const s = subHeaders[subHeaderKeys[i]];
            for (j = 0; j < s.entryCount; j++) {
              const charCode = (i << 8) + j + s.firstCode;
              file.pos = s.idRangePos + 2 * j;
              glyphId = file.getUint16();
              if (glyphId !== 0) {
                glyphId = (glyphId + s.idDelta) % 65536;
              }
              mappings.push({
                charCode,
                glyphId,
              });
            }
          }
        }
      } else if (format === 4) {
        file.skip(2 + 2); // length + language

        // re-creating the table in format 4 since the encoding
        // might be changed
        const segCount = file.getUint16() >> 1;
        file.skip(6); // skipping range fields
        const segments = [];
        let segIndex;
        for (segIndex = 0; segIndex < segCount; segIndex++) {
          segments.push({ end: file.getUint16() });
        }
        file.skip(2);
        for (segIndex = 0; segIndex < segCount; segIndex++) {
          segments[segIndex].start = file.getUint16();
        }

        for (segIndex = 0; segIndex < segCount; segIndex++) {
          segments[segIndex].delta = file.getUint16();
        }

        let offsetsCount = 0,
          offsetIndex;
        for (segIndex = 0; segIndex < segCount; segIndex++) {
          segment = segments[segIndex];
          const rangeOffset = file.getUint16();
          if (!rangeOffset) {
            segment.offsetIndex = -1;
            continue;
          }

          offsetIndex = (rangeOffset >> 1) - (segCount - segIndex);
          segment.offsetIndex = offsetIndex;
          offsetsCount = Math.max(
            offsetsCount,
            offsetIndex + segment.end - segment.start + 1
          );
        }

        const offsets = [];
        for (j = 0; j < offsetsCount; j++) {
          offsets.push(file.getUint16());
        }

        for (segIndex = 0; segIndex < segCount; segIndex++) {
          segment = segments[segIndex];
          start = segment.start;
          const end = segment.end;
          const delta = segment.delta;
          offsetIndex = segment.offsetIndex;

          for (j = start; j <= end; j++) {
            if (j === 0xffff) {
              continue;
            }

            glyphId = offsetIndex < 0 ? j : offsets[offsetIndex + j - start];
            glyphId = (glyphId + delta) & 0xffff;
            mappings.push({
              charCode: j,
              glyphId,
            });
          }
        }
      } else if (format === 6) {
        file.skip(2 + 2); // length + language

        // Format 6 is a 2-bytes dense mapping, which means the font data
        // lives glue together even if they are pretty far in the unicode
        // table. (This looks weird, so I can have missed something), this
        // works on Linux but seems to fails on Mac so let's rewrite the
        // cmap table to a 3-1-4 style
        const firstCode = file.getUint16();
        const entryCount = file.getUint16();

        for (j = 0; j < entryCount; j++) {
          glyphId = file.getUint16();
          const charCode = firstCode + j;

          mappings.push({
            charCode,
            glyphId,
          });
        }
      } else if (format === 12) {
        file.skip(2 + 4 + 4); // reserved + length + language

        const nGroups = file.getInt32() >>> 0;
        for (j = 0; j < nGroups; j++) {
          const startCharCode = file.getInt32() >>> 0;
          const endCharCode = file.getInt32() >>> 0;
          let glyphCode = file.getInt32() >>> 0;

          for (
            let charCode = startCharCode;
            charCode <= endCharCode;
            charCode++
          ) {
            mappings.push({
              charCode,
              glyphId: glyphCode++,
            });
          }
        }
      } else {
        warn("cmap table has unsupported format: " + format);
        return {
          platformId: -1,
          encodingId: -1,
          mappings: [],
          hasShortCmap: false,
        };
      }

      // removing duplicate entries
      mappings.sort((a, b) => a.charCode - b.charCode);
      const finalMappings = [],
        seenCharCodes = new Set();
      for (const map of mappings) {
        const { charCode } = map;

        if (seenCharCodes.has(charCode)) {
          continue;
        }
        seenCharCodes.add(charCode);
        finalMappings.push(map);
      }

      return {
        platformId: potentialTable.platformId,
        encodingId: potentialTable.encodingId,
        mappings: finalMappings,
        hasShortCmap,
      };
    }

    function sanitizeMetrics(
      file,
      header,
      metrics,
      headTable,
      numGlyphs,
      dupFirstEntry
    ) {
      if (!header) {
        if (metrics) {
          metrics.data = null;
        }
        return;
      }

      file.pos = (file.start || 0) + header.offset;
      file.pos += 4; // version
      file.pos += 2; // ascent
      file.pos += 2; // descent
      file.pos += 2; // linegap
      file.pos += 2; // adv_width_max
      file.pos += 2; // min_sb1
      file.pos += 2; // min_sb2
      file.pos += 2; // max_extent
      file.pos += 2; // caret_slope_rise
      file.pos += 2; // caret_slope_run
      const caretOffset = file.getUint16();
      file.pos += 8; // reserved
      file.pos += 2; // format
      let numOfMetrics = file.getUint16();

      if (caretOffset !== 0) {
        const macStyle = int16(headTable.data[44], headTable.data[45]);
        if (!(macStyle & 2)) {
          // Suppress OTS warnings about the `caretOffset` in the hhea-table.
          header.data[22] = 0;
          header.data[23] = 0;
        }
      }

      if (numOfMetrics > numGlyphs) {
        info(
          `The numOfMetrics (${numOfMetrics}) should not be ` +
            `greater than the numGlyphs (${numGlyphs}).`
        );
        // Reduce numOfMetrics if it is greater than numGlyphs
        numOfMetrics = numGlyphs;
        header.data[34] = (numOfMetrics & 0xff00) >> 8;
        header.data[35] = numOfMetrics & 0x00ff;
      }

      const numOfSidebearings = numGlyphs - numOfMetrics;
      const numMissing =
        numOfSidebearings - ((metrics.length - numOfMetrics * 4) >> 1);

      if (numMissing > 0) {
        // For each missing glyph, we set both the width and lsb to 0 (zero).
        // Since we need to add two properties for each glyph, this explains
        // the use of |numMissing * 2| when initializing the typed array.
        const entries = new Uint8Array(metrics.length + numMissing * 2);
        entries.set(metrics.data);
        if (dupFirstEntry) {
          // Set the sidebearing value of the duplicated glyph.
          entries[metrics.length] = metrics.data[2];
          entries[metrics.length + 1] = metrics.data[3];
        }
        metrics.data = entries;
      }
    }

    function sanitizeGlyph(
      source,
      sourceStart,
      sourceEnd,
      dest,
      destStart,
      hintsValid
    ) {
      const glyphProfile = {
        length: 0,
        sizeOfInstructions: 0,
      };
      if (
        sourceStart < 0 ||
        sourceStart >= source.length ||
        sourceEnd > source.length ||
        sourceEnd - sourceStart <= 12
      ) {
        // If the offsets are wrong or the glyph is too small, remove it.
        return glyphProfile;
      }
      const glyf = source.subarray(sourceStart, sourceEnd);

      // Sanitize the glyph bounding box.
      const xMin = signedInt16(glyf[2], glyf[3]);
      const yMin = signedInt16(glyf[4], glyf[5]);
      const xMax = signedInt16(glyf[6], glyf[7]);
      const yMax = signedInt16(glyf[8], glyf[9]);

      if (xMin > xMax) {
        writeSignedInt16(glyf, 2, xMax);
        writeSignedInt16(glyf, 6, xMin);
      }
      if (yMin > yMax) {
        writeSignedInt16(glyf, 4, yMax);
        writeSignedInt16(glyf, 8, yMin);
      }

      const contoursCount = signedInt16(glyf[0], glyf[1]);
      if (contoursCount < 0) {
        if (contoursCount < -1) {
          // OTS doesn't like contour count to be less than -1.
          // The glyph data offsets are very likely wrong and
          // having something lower than -1, very likely, implies
          // to have some garbage data.
          return glyphProfile;
        }
        // complex glyph, writing as is
        dest.set(glyf, destStart);
        glyphProfile.length = glyf.length;
        return glyphProfile;
      }

      let i,
        j = 10,
        flagsCount = 0;
      for (i = 0; i < contoursCount; i++) {
        const endPoint = (glyf[j] << 8) | glyf[j + 1];
        flagsCount = endPoint + 1;
        j += 2;
      }
      // skipping instructions
      const instructionsStart = j;
      const instructionsLength = (glyf[j] << 8) | glyf[j + 1];
      glyphProfile.sizeOfInstructions = instructionsLength;
      j += 2 + instructionsLength;
      const instructionsEnd = j;
      // validating flags
      let coordinatesLength = 0;
      for (i = 0; i < flagsCount; i++) {
        const flag = glyf[j++];
        if (flag & 0xc0) {
          // reserved flags must be zero, cleaning up
          glyf[j - 1] = flag & 0x3f;
        }
        let xLength = 2;
        if (flag & 2) {
          xLength = 1;
        } else if (flag & 16) {
          xLength = 0;
        }
        let yLength = 2;
        if (flag & 4) {
          yLength = 1;
        } else if (flag & 32) {
          yLength = 0;
        }
        const xyLength = xLength + yLength;
        coordinatesLength += xyLength;
        if (flag & 8) {
          const repeat = glyf[j++];
          if (repeat === 0) {
            // The repeat count should be non-zero when the repeat flag is set.
            glyf[j - 1] ^= 8;
          }
          i += repeat;
          coordinatesLength += repeat * xyLength;
        }
      }
      // glyph without coordinates will be rejected
      if (coordinatesLength === 0) {
        return glyphProfile;
      }
      let glyphDataLength = j + coordinatesLength;
      if (glyphDataLength > glyf.length) {
        // not enough data for coordinates
        return glyphProfile;
      }
      if (!hintsValid && instructionsLength > 0) {
        dest.set(glyf.subarray(0, instructionsStart), destStart);
        dest.set([0, 0], destStart + instructionsStart);
        dest.set(
          glyf.subarray(instructionsEnd, glyphDataLength),
          destStart + instructionsStart + 2
        );
        glyphDataLength -= instructionsLength;
        if (glyf.length - glyphDataLength > 3) {
          glyphDataLength = (glyphDataLength + 3) & ~3;
        }
        glyphProfile.length = glyphDataLength;
        return glyphProfile;
      }
      if (glyf.length - glyphDataLength > 3) {
        // truncating and aligning to 4 bytes the long glyph data
        glyphDataLength = (glyphDataLength + 3) & ~3;
        dest.set(glyf.subarray(0, glyphDataLength), destStart);
        glyphProfile.length = glyphDataLength;
        return glyphProfile;
      }
      // glyph data is fine
      dest.set(glyf, destStart);
      glyphProfile.length = glyf.length;
      return glyphProfile;
    }

    function sanitizeHead(head, numGlyphs, locaLength) {
      const data = head.data;

      // Validate version:
      // Should always be 0x00010000
      const version = int32(data[0], data[1], data[2], data[3]);
      if (version >> 16 !== 1) {
        info("Attempting to fix invalid version in head table: " + version);
        data[0] = 0;
        data[1] = 1;
        data[2] = 0;
        data[3] = 0;
      }

      const indexToLocFormat = int16(data[50], data[51]);
      if (indexToLocFormat < 0 || indexToLocFormat > 1) {
        info(
          "Attempting to fix invalid indexToLocFormat in head table: " +
            indexToLocFormat
        );

        // The value of indexToLocFormat should be 0 if the loca table
        // consists of short offsets, and should be 1 if the loca table
        // consists of long offsets.
        //
        // The number of entries in the loca table should be numGlyphs + 1.
        //
        // Using this information, we can work backwards to deduce if the
        // size of each offset in the loca table, and thus figure out the
        // appropriate value for indexToLocFormat.

        const numGlyphsPlusOne = numGlyphs + 1;
        if (locaLength === numGlyphsPlusOne << 1) {
          // 0x0000 indicates the loca table consists of short offsets
          data[50] = 0;
          data[51] = 0;
        } else if (locaLength === numGlyphsPlusOne << 2) {
          // 0x0001 indicates the loca table consists of long offsets
          data[50] = 0;
          data[51] = 1;
        } else {
          throw new FormatError(
            "Could not fix indexToLocFormat: " + indexToLocFormat
          );
        }
      }
    }

    function sanitizeGlyphLocations(
      loca,
      glyf,
      numGlyphs,
      isGlyphLocationsLong,
      hintsValid,
      dupFirstEntry,
      maxSizeOfInstructions
    ) {
      let itemSize, itemDecode, itemEncode;
      if (isGlyphLocationsLong) {
        itemSize = 4;
        itemDecode = function fontItemDecodeLong(data, offset) {
          return (
            (data[offset] << 24) |
            (data[offset + 1] << 16) |
            (data[offset + 2] << 8) |
            data[offset + 3]
          );
        };
        itemEncode = function fontItemEncodeLong(data, offset, value) {
          data[offset] = (value >>> 24) & 0xff;
          data[offset + 1] = (value >> 16) & 0xff;
          data[offset + 2] = (value >> 8) & 0xff;
          data[offset + 3] = value & 0xff;
        };
      } else {
        itemSize = 2;
        itemDecode = function fontItemDecode(data, offset) {
          return (data[offset] << 9) | (data[offset + 1] << 1);
        };
        itemEncode = function fontItemEncode(data, offset, value) {
          data[offset] = (value >> 9) & 0xff;
          data[offset + 1] = (value >> 1) & 0xff;
        };
      }
      // The first glyph is duplicated.
      const numGlyphsOut = dupFirstEntry ? numGlyphs + 1 : numGlyphs;
      const locaDataSize = itemSize * (1 + numGlyphsOut);
      // Resize loca table to account for duplicated glyph.
      const locaData = new Uint8Array(locaDataSize);
      locaData.set(loca.data.subarray(0, locaDataSize));
      loca.data = locaData;
      // removing the invalid glyphs
      const oldGlyfData = glyf.data;
      const oldGlyfDataLength = oldGlyfData.length;
      const newGlyfData = new Uint8Array(oldGlyfDataLength);

      // The spec says the offsets should be in ascending order, however
      // this is not true for some fonts or they use the offset of 0 to mark a
      // glyph as missing. OTS requires the offsets to be in order and not to
      // be zero, so we must sort and rebuild the loca table and potentially
      // re-arrange the glyf data.
      let i, j;
      const locaEntries = [];
      // There are numGlyphs + 1 loca table entries.
      for (i = 0, j = 0; i < numGlyphs + 1; i++, j += itemSize) {
        let offset = itemDecode(locaData, j);
        if (offset > oldGlyfDataLength) {
          offset = oldGlyfDataLength;
        }
        locaEntries.push({
          index: i,
          offset,
          endOffset: 0,
        });
      }
      locaEntries.sort((a, b) => a.offset - b.offset);
      // Now the offsets are sorted, calculate the end offset of each glyph.
      // The last loca entry's endOffset is not calculated since it's the end
      // of the data and will be stored on the previous entry's endOffset.
      for (i = 0; i < numGlyphs; i++) {
        locaEntries[i].endOffset = locaEntries[i + 1].offset;
      }
      // Re-sort so glyphs aren't out of order.
      locaEntries.sort((a, b) => a.index - b.index);
      // Calculate the endOffset of the "first" glyph correctly when there are
      // *multiple* empty ones at the start of the data (fixes issue14618.pdf).
      for (i = 0; i < numGlyphs; i++) {
        const { offset, endOffset } = locaEntries[i];
        if (offset !== 0 || endOffset !== 0) {
          break;
        }
        const nextOffset = locaEntries[i + 1].offset;
        if (nextOffset === 0) {
          continue;
        }
        locaEntries[i].endOffset = nextOffset;
        break;
      }

      // If the last offset is 0 in the loca table then we can't compute the
      // endOffset for the last glyph. So in such a case we set the endOffset
      // to the end of the data (fixes issue #17671).
      const last = locaEntries.at(-2);
      if (last.offset !== 0 && last.endOffset === 0) {
        last.endOffset = oldGlyfDataLength;
      }

      const missingGlyphs = Object.create(null);
      let writeOffset = 0;
      itemEncode(locaData, 0, writeOffset);
      for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
        const glyphProfile = sanitizeGlyph(
          oldGlyfData,
          locaEntries[i].offset,
          locaEntries[i].endOffset,
          newGlyfData,
          writeOffset,
          hintsValid
        );
        const newLength = glyphProfile.length;
        if (newLength === 0) {
          missingGlyphs[i] = true;
        }
        if (glyphProfile.sizeOfInstructions > maxSizeOfInstructions) {
          maxSizeOfInstructions = glyphProfile.sizeOfInstructions;
        }
        writeOffset += newLength;
        itemEncode(locaData, j, writeOffset);
      }

      if (writeOffset === 0) {
        // glyf table cannot be empty -- redoing the glyf and loca tables
        // to have single glyph with one point
        const simpleGlyph = new Uint8Array([
          0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0,
        ]);
        for (i = 0, j = itemSize; i < numGlyphsOut; i++, j += itemSize) {
          itemEncode(locaData, j, simpleGlyph.length);
        }
        glyf.data = simpleGlyph;
      } else if (dupFirstEntry) {
        // Browsers will not display a glyph at position 0. Typically glyph 0
        // is notdef, but a number of fonts put a valid glyph there so it must
        // be duplicated and appended.
        const firstEntryLength = itemDecode(locaData, itemSize);
        if (newGlyfData.length > firstEntryLength + writeOffset) {
          glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset);
        } else {
          glyf.data = new Uint8Array(firstEntryLength + writeOffset);
          glyf.data.set(newGlyfData.subarray(0, writeOffset));
        }
        glyf.data.set(newGlyfData.subarray(0, firstEntryLength), writeOffset);
        itemEncode(
          loca.data,
          locaData.length - itemSize,
          writeOffset + firstEntryLength
        );
      } else {
        glyf.data = newGlyfData.subarray(0, writeOffset);
      }
      return {
        missingGlyphs,
        maxSizeOfInstructions,
      };
    }

    function readPostScriptTable(post, propertiesObj, maxpNumGlyphs) {
      const start = (font.start || 0) + post.offset;
      font.pos = start;

      const length = post.length,
        end = start + length;
      const version = font.getInt32();
      // skip rest to the tables
      font.skip(28);

      let glyphNames;
      let valid = true;
      let i;

      switch (version) {
        case 0x00010000:
          glyphNames = MacStandardGlyphOrdering;
          break;
        case 0x00020000:
          const numGlyphs = font.getUint16();
          if (numGlyphs !== maxpNumGlyphs) {
            valid = false;
            break;
          }
          const glyphNameIndexes = [];
          for (i = 0; i < numGlyphs; ++i) {
            const index = font.getUint16();
            if (index >= 32768) {
              valid = false;
              break;
            }
            glyphNameIndexes.push(index);
          }
          if (!valid) {
            break;
          }
          const customNames = [],
            strBuf = [];
          while (font.pos < end) {
            const stringLength = font.getByte();
            strBuf.length = stringLength;
            for (i = 0; i < stringLength; ++i) {
              strBuf[i] = String.fromCharCode(font.getByte());
            }
            customNames.push(strBuf.join(""));
          }
          glyphNames = [];
          for (i = 0; i < numGlyphs; ++i) {
            const j = glyphNameIndexes[i];
            if (j < 258) {
              glyphNames.push(MacStandardGlyphOrdering[j]);
              continue;
            }
            glyphNames.push(customNames[j - 258]);
          }
          break;
        case 0x00030000:
          break;
        default:
          warn("Unknown/unsupported post table version " + version);
          valid = false;
          if (propertiesObj.defaultEncoding) {
            glyphNames = propertiesObj.defaultEncoding;
          }
          break;
      }
      propertiesObj.glyphNames = glyphNames;
      return valid;
    }

    function readNameTable(nameTable) {
      const start = (font.start || 0) + nameTable.offset;
      font.pos = start;

      const names = [[], []],
        records = [];
      const length = nameTable.length,
        end = start + length;
      const format = font.getUint16();
      const FORMAT_0_HEADER_LENGTH = 6;
      if (format !== 0 || length < FORMAT_0_HEADER_LENGTH) {
        // unsupported name table format or table "too" small
        return [names, records];
      }
      const numRecords = font.getUint16();
      const stringsStart = font.getUint16();
      const NAME_RECORD_LENGTH = 12;
      let i, ii;

      for (i = 0; i < numRecords && font.pos + NAME_RECORD_LENGTH <= end; i++) {
        const r = {
          platform: font.getUint16(),
          encoding: font.getUint16(),
          language: font.getUint16(),
          name: font.getUint16(),
          length: font.getUint16(),
          offset: font.getUint16(),
        };
        // using only Macintosh and Windows platform/encoding names
        if (isMacNameRecord(r) || isWinNameRecord(r)) {
          records.push(r);
        }
      }
      for (i = 0, ii = records.length; i < ii; i++) {
        const record = records[i];
        if (record.length <= 0) {
          continue; // Nothing to process, ignoring.
        }
        const pos = start + stringsStart + record.offset;
        if (pos + record.length > end) {
          continue; // outside of name table, ignoring
        }
        font.pos = pos;
        const nameIndex = record.name;
        if (record.encoding) {
          // unicode
          let str = "";
          for (let j = 0, jj = record.length; j < jj; j += 2) {
            str += String.fromCharCode(font.getUint16());
          }
          names[1][nameIndex] = str;
        } else {
          names[0][nameIndex] = font.getString(record.length);
        }
      }
      return [names, records];
    }

    // prettier-ignore
    const TTOpsStackDeltas = [
      0, 0, 0, 0, 0, 0, 0, 0, -2, -2, -2, -2, 0, 0, -2, -5,
      -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, -1, -1,
      1, -1, -999, 0, 1, 0, -1, -2, 0, -1, -2, -1, -1, 0, -1, -1,
      0, 0, -999, -999, -1, -1, -1, -1, -2, -999, -2, -2, -999, 0, -2, -2,
      0, 0, -2, 0, -2, 0, 0, 0, -2, -1, -1, 1, 1, 0, 0, -1,
      -1, -1, -1, -1, -1, -1, 0, 0, -1, 0, -1, -1, 0, -999, -1, -1,
      -1, -1, -1, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      -2, -999, -999, -999, -999, -999, -1, -1, -2, -2, 0, 0, 0, 0, -1, -1,
      -999, -2, -2, 0, 0, -1, -2, -2, 0, 0, 0, -1, -1, -1, -2];
    // 0xC0-DF == -1 and 0xE0-FF == -2

    function sanitizeTTProgram(table, ttContext) {
      let data = table.data;
      let i = 0,
        j,
        n,
        b,
        funcId,
        pc,
        lastEndf = 0,
        lastDeff = 0;
      const stack = [];
      const callstack = [];
      const functionsCalled = [];
      let tooComplexToFollowFunctions = ttContext.tooComplexToFollowFunctions;
      let inFDEF = false,
        ifLevel = 0,
        inELSE = 0;
      for (let ii = data.length; i < ii; ) {
        const op = data[i++];
        // The TrueType instruction set docs can be found at
        // https://developer.apple.com/fonts/TTRefMan/RM05/Chap5.html
        if (op === 0x40) {
          // NPUSHB - pushes n bytes
          n = data[i++];
          if (inFDEF || inELSE) {
            i += n;
          } else {
            for (j = 0; j < n; j++) {
              stack.push(data[i++]);
            }
          }
        } else if (op === 0x41) {
          // NPUSHW - pushes n words
          n = data[i++];
          if (inFDEF || inELSE) {
            i += n * 2;
          } else {
            for (j = 0; j < n; j++) {
              b = data[i++];
              stack.push((b << 8) | data[i++]);
            }
          }
        } else if ((op & 0xf8) === 0xb0) {
          // PUSHB - pushes bytes
          n = op - 0xb0 + 1;
          if (inFDEF || inELSE) {
            i += n;
          } else {
            for (j = 0; j < n; j++) {
              stack.push(data[i++]);
            }
          }
        } else if ((op & 0xf8) === 0xb8) {
          // PUSHW - pushes words
          n = op - 0xb8 + 1;
          if (inFDEF || inELSE) {
            i += n * 2;
          } else {
            for (j = 0; j < n; j++) {
              b = data[i++];
              stack.push(signedInt16(b, data[i++]));
            }
          }
        } else if (op === 0x2b && !tooComplexToFollowFunctions) {
          // CALL
          if (!inFDEF && !inELSE) {
            // collecting information about which functions are used
            funcId = stack.at(-1);
            if (isNaN(funcId)) {
              info("TT: CALL empty stack (or invalid entry).");
            } else {
              ttContext.functionsUsed[funcId] = true;
              if (funcId in ttContext.functionsStackDeltas) {
                const newStackLength =
                  stack.length + ttContext.functionsStackDeltas[funcId];
                if (newStackLength < 0) {
                  warn("TT: CALL invalid functions stack delta.");
                  ttContext.hintsValid = false;
                  return;
                }
                stack.length = newStackLength;
              } else if (
                funcId in ttContext.functionsDefined &&
                !functionsCalled.includes(funcId)
              ) {
                callstack.push({ data, i, stackTop: stack.length - 1 });
                functionsCalled.push(funcId);
                pc = ttContext.functionsDefined[funcId];
                if (!pc) {
                  warn("TT: CALL non-existent function");
                  ttContext.hintsValid = false;
                  return;
                }
                data = pc.data;
                i = pc.i;
              }
            }
          }
        } else if (op === 0x2c && !tooComplexToFollowFunctions) {
          // FDEF
          if (inFDEF || inELSE) {
            warn("TT: nested FDEFs not allowed");
            tooComplexToFollowFunctions = true;
          }
          inFDEF = true;
          // collecting information about which functions are defined
          lastDeff = i;
          funcId = stack.pop();
          ttContext.functionsDefined[funcId] = { data, i };
        } else if (op === 0x2d) {
          // ENDF - end of function
          if (inFDEF) {
            inFDEF = false;
            lastEndf = i;
          } else {
            pc = callstack.pop();
            if (!pc) {
              warn("TT: ENDF bad stack");
              ttContext.hintsValid = false;
              return;
            }
            funcId = functionsCalled.pop();
            data = pc.data;
            i = pc.i;
            ttContext.functionsStackDeltas[funcId] = stack.length - pc.stackTop;
          }
        } else if (op === 0x89) {
          // IDEF - instruction definition
          if (inFDEF || inELSE) {
            warn("TT: nested IDEFs not allowed");
            tooComplexToFollowFunctions = true;
          }
          inFDEF = true;
          // recording it as a function to track ENDF
          lastDeff = i;
        } else if (op === 0x58) {
          // IF
          ++ifLevel;
        } else if (op === 0x1b) {
          // ELSE
          inELSE = ifLevel;
        } else if (op === 0x59) {
          // EIF
          if (inELSE === ifLevel) {
            inELSE = 0;
          }
          --ifLevel;
        } else if (op === 0x1c) {
          // JMPR
          if (!inFDEF && !inELSE) {
            const offset = stack.at(-1);
            // only jumping forward to prevent infinite loop
            if (offset > 0) {
              i += offset - 1;
            }
          }
        }
        // Adjusting stack not extactly, but just enough to get function id
        if (!inFDEF && !inELSE) {
          let stackDelta = 0;
          if (op <= 0x8e) {
            stackDelta = TTOpsStackDeltas[op];
          } else if (op >= 0xc0 && op <= 0xdf) {
            stackDelta = -1;
          } else if (op >= 0xe0) {
            stackDelta = -2;
          }
          if (op >= 0x71 && op <= 0x75) {
            n = stack.pop();
            if (!isNaN(n)) {
              stackDelta = -n * 2;
            }
          }
          while (stackDelta < 0 && stack.length > 0) {
            stack.pop();
            stackDelta++;
          }
          while (stackDelta > 0) {
            stack.push(NaN); // pushing any number into stack
            stackDelta--;
          }
        }
      }
      ttContext.tooComplexToFollowFunctions = tooComplexToFollowFunctions;
      const content = [data];
      if (i > data.length) {
        content.push(new Uint8Array(i - data.length));
      }
      if (lastDeff > lastEndf) {
        warn("TT: complementing a missing function tail");
        // new function definition started, but not finished
        // complete function by [CLEAR, ENDF]
        content.push(new Uint8Array([0x22, 0x2d]));
      }
      foldTTTable(table, content);
    }

    function checkInvalidFunctions(ttContext, maxFunctionDefs) {
      if (ttContext.tooComplexToFollowFunctions) {
        return;
      }
      if (ttContext.functionsDefined.length > maxFunctionDefs) {
        warn("TT: more functions defined than expected");
        ttContext.hintsValid = false;
        return;
      }
      for (let j = 0, jj = ttContext.functionsUsed.length; j < jj; j++) {
        if (j > maxFunctionDefs) {
          warn("TT: invalid function id: " + j);
          ttContext.hintsValid = false;
          return;
        }
        if (ttContext.functionsUsed[j] && !ttContext.functionsDefined[j]) {
          warn("TT: undefined function: " + j);
          ttContext.hintsValid = false;
          return;
        }
      }
    }

    function foldTTTable(table, content) {
      if (content.length > 1) {
        // concatenating the content items
        let newLength = 0;
        let j, jj;
        for (j = 0, jj = content.length; j < jj; j++) {
          newLength += content[j].length;
        }
        newLength = (newLength + 3) & ~3;
        const result = new Uint8Array(newLength);
        let pos = 0;
        for (j = 0, jj = content.length; j < jj; j++) {
          result.set(content[j], pos);
          pos += content[j].length;
        }
        table.data = result;
        table.length = newLength;
      }
    }

    function sanitizeTTPrograms(fpgm, prep, cvt, maxFunctionDefs) {
      const ttContext = {
        functionsDefined: [],
        functionsUsed: [],
        functionsStackDeltas: [],
        tooComplexToFollowFunctions: false,
        hintsValid: true,
      };
      if (fpgm) {
        sanitizeTTProgram(fpgm, ttContext);
      }
      if (prep) {
        sanitizeTTProgram(prep, ttContext);
      }
      if (fpgm) {
        checkInvalidFunctions(ttContext, maxFunctionDefs);
      }
      if (cvt && cvt.length & 1) {
        const cvtData = new Uint8Array(cvt.length + 1);
        cvtData.set(cvt.data);
        cvt.data = cvtData;
      }
      return ttContext.hintsValid;
    }

    // The following steps modify the original font data, making copy
    font = new Stream(new Uint8Array(font.getBytes()));

    let header, tables;
    if (isTrueTypeCollectionFile(font)) {
      const ttcData = readTrueTypeCollectionData(font, this.name);
      header = ttcData.header;
      tables = ttcData.tables;
    } else {
      header = readOpenTypeHeader(font);
      tables = readTables(font, header.numTables);
    }
    let cff, cffFile;

    const isTrueType = !tables["CFF "];
    if (!isTrueType) {
      const isComposite =
        properties.composite &&
        (properties.cidToGidMap?.length > 0 ||
          !(properties.cMap instanceof IdentityCMap));
      // OpenType font (skip composite fonts with non-default glyph mapping).
      if (
        (header.version === "OTTO" && !isComposite) ||
        !tables.head ||
        !tables.hhea ||
        !tables.maxp ||
        !tables.post
      ) {
        // No major tables: throwing everything at `CFFFont`.
        cffFile = new Stream(tables["CFF "].data);
        cff = new CFFFont(cffFile, properties);

        return this.convert(name, cff, properties);
      }

      delete tables.glyf;
      delete tables.loca;
      delete tables.fpgm;
      delete tables.prep;
      delete tables["cvt "];
      this.isOpenType = true;
    } else {
      if (!tables.loca) {
        throw new FormatError('Required "loca" table is not found');
      }
      if (!tables.glyf) {
        warn('Required "glyf" table is not found -- trying to recover.');
        // Note: We use `sanitizeGlyphLocations` to add dummy glyf data below.
        tables.glyf = {
          tag: "glyf",
          data: new Uint8Array(0),
        };
      }
      this.isOpenType = false;
    }

    if (!tables.maxp) {
      throw new FormatError('Required "maxp" table is not found');
    }

    font.pos = (font.start || 0) + tables.maxp.offset;
    let version = font.getInt32();
    const numGlyphs = font.getUint16();

    if (version !== 0x00010000 && version !== 0x00005000) {
      // https://learn.microsoft.com/en-us/typography/opentype/spec/maxp
      if (tables.maxp.length === 6) {
        version = 0x0005000;
      } else if (tables.maxp.length >= 32) {
        version = 0x00010000;
      } else {
        throw new FormatError(`"maxp" table has a wrong version number`);
      }
      writeUint32(tables.maxp.data, 0, version);
    }

    if (properties.scaleFactors?.length === numGlyphs && isTrueType) {
      const { scaleFactors } = properties;
      const isGlyphLocationsLong = int16(
        tables.head.data[50],
        tables.head.data[51]
      );

      const glyphs = new GlyfTable({
        glyfTable: tables.glyf.data,
        isGlyphLocationsLong,
        locaTable: tables.loca.data,
        numGlyphs,
      });
      glyphs.scale(scaleFactors);

      const { glyf, loca, isLocationLong } = glyphs.write();
      tables.glyf.data = glyf;
      tables.loca.data = loca;

      if (isLocationLong !== !!isGlyphLocationsLong) {
        tables.head.data[50] = 0;
        tables.head.data[51] = isLocationLong ? 1 : 0;
      }

      const metrics = tables.hmtx.data;

      for (let i = 0; i < numGlyphs; i++) {
        const j = 4 * i;
        const advanceWidth = Math.round(
          scaleFactors[i] * int16(metrics[j], metrics[j + 1])
        );
        metrics[j] = (advanceWidth >> 8) & 0xff;
        metrics[j + 1] = advanceWidth & 0xff;
        const lsb = Math.round(
          scaleFactors[i] * signedInt16(metrics[j + 2], metrics[j + 3])
        );
        writeSignedInt16(metrics, j + 2, lsb);
      }
    }

    // Glyph 0 is duplicated and appended.
    let numGlyphsOut = numGlyphs + 1;
    let dupFirstEntry = true;
    if (numGlyphsOut > 0xffff) {
      dupFirstEntry = false;
      numGlyphsOut = numGlyphs;
      warn("Not enough space in glyfs to duplicate first glyph.");
    }
    let maxFunctionDefs = 0;
    let maxSizeOfInstructions = 0;
    if (version >= 0x00010000 && tables.maxp.length >= 32) {
      // maxZones can be invalid
      font.pos += 8;
      const maxZones = font.getUint16();
      if (maxZones > 2) {
        // reset to 2 if font has invalid maxZones
        tables.maxp.data[14] = 0;
        tables.maxp.data[15] = 2;
      }
      font.pos += 4;
      maxFunctionDefs = font.getUint16();
      font.pos += 4;
      maxSizeOfInstructions = font.getUint16();
    }

    tables.maxp.data[4] = numGlyphsOut >> 8;
    tables.maxp.data[5] = numGlyphsOut & 255;

    const hintsValid = sanitizeTTPrograms(
      tables.fpgm,
      tables.prep,
      tables["cvt "],
      maxFunctionDefs
    );
    if (!hintsValid) {
      delete tables.fpgm;
      delete tables.prep;
      delete tables["cvt "];
    }

    // Ensure the hmtx table contains the advance width and
    // sidebearings information for numGlyphs in the maxp table
    sanitizeMetrics(
      font,
      tables.hhea,
      tables.hmtx,
      tables.head,
      numGlyphsOut,
      dupFirstEntry
    );

    if (!tables.head) {
      throw new FormatError('Required "head" table is not found');
    }

    sanitizeHead(tables.head, numGlyphs, isTrueType ? tables.loca.length : 0);

    let missingGlyphs = Object.create(null);
    if (isTrueType) {
      const isGlyphLocationsLong = int16(
        tables.head.data[50],
        tables.head.data[51]
      );
      const glyphsInfo = sanitizeGlyphLocations(
        tables.loca,
        tables.glyf,
        numGlyphs,
        isGlyphLocationsLong,
        hintsValid,
        dupFirstEntry,
        maxSizeOfInstructions
      );
      missingGlyphs = glyphsInfo.missingGlyphs;

      // Some fonts have incorrect maxSizeOfInstructions values, so we use
      // the computed value instead.
      if (version >= 0x00010000 && tables.maxp.length >= 32) {
        tables.maxp.data[26] = glyphsInfo.maxSizeOfInstructions >> 8;
        tables.maxp.data[27] = glyphsInfo.maxSizeOfInstructions & 255;
      }
    }
    if (!tables.hhea) {
      throw new FormatError('Required "hhea" table is not found');
    }

    // Sanitizer reduces the glyph advanceWidth to the maxAdvanceWidth
    // Sometimes it's 0. That needs to be fixed
    if (tables.hhea.data[10] === 0 && tables.hhea.data[11] === 0) {
      tables.hhea.data[10] = 0xff;
      tables.hhea.data[11] = 0xff;
    }

    // Extract some more font properties from the OpenType head and
    // hhea tables; yMin and descent value are always negative.
    const metricsOverride = {
      unitsPerEm: int16(tables.head.data[18], tables.head.data[19]),
      yMax: signedInt16(tables.head.data[42], tables.head.data[43]),
      yMin: signedInt16(tables.head.data[38], tables.head.data[39]),
      ascent: signedInt16(tables.hhea.data[4], tables.hhea.data[5]),
      descent: signedInt16(tables.hhea.data[6], tables.hhea.data[7]),
      lineGap: signedInt16(tables.hhea.data[8], tables.hhea.data[9]),
    };

    // PDF FontDescriptor metrics lie -- using data from actual font.
    this.ascent = metricsOverride.ascent / metricsOverride.unitsPerEm;
    this.descent = metricsOverride.descent / metricsOverride.unitsPerEm;
    this.lineGap = metricsOverride.lineGap / metricsOverride.unitsPerEm;

    if (this.cssFontInfo?.lineHeight) {
      this.lineHeight = this.cssFontInfo.metrics.lineHeight;
      this.lineGap = this.cssFontInfo.metrics.lineGap;
    } else {
      this.lineHeight = this.ascent - this.descent + this.lineGap;
    }

    // The 'post' table has glyphs names.
    if (tables.post) {
      readPostScriptTable(tables.post, properties, numGlyphs);
    }

    // The original 'post' table is not needed, replace it.
    tables.post = {
      tag: "post",
      data: createPostTable(properties),
    };

    const charCodeToGlyphId = Object.create(null);

    // Helper function to try to skip mapping of empty glyphs.
    function hasGlyph(glyphId) {
      return !missingGlyphs[glyphId];
    }

    if (properties.composite) {
      const cidToGidMap = properties.cidToGidMap || [];
      const isCidToGidMapEmpty = cidToGidMap.length === 0;

      properties.cMap.forEach(function (charCode, cid) {
        if (typeof cid === "string") {
          cid = convertCidString(charCode, cid, /* shouldThrow = */ true);
        }
        if (cid > 0xffff) {
          throw new FormatError("Max size of CID is 65,535");
        }
        let glyphId = -1;
        if (isCidToGidMapEmpty) {
          glyphId = cid;
        } else if (cidToGidMap[cid] !== undefined) {
          glyphId = cidToGidMap[cid];
        }

        if (glyphId >= 0 && glyphId < numGlyphs && hasGlyph(glyphId)) {
          charCodeToGlyphId[charCode] = glyphId;
        }
      });
    } else {
      // Most of the following logic in this code branch is based on the
      // 9.6.6.4 of the PDF spec.
      const cmapTable = readCmapTable(
        tables.cmap,
        font,
        this.isSymbolicFont,
        properties.hasEncoding
      );
      const cmapPlatformId = cmapTable.platformId;
      const cmapEncodingId = cmapTable.encodingId;
      const cmapMappings = cmapTable.mappings;
      let baseEncoding = [],
        forcePostTable = false;
      if (
        properties.hasEncoding &&
        (properties.baseEncodingName === "MacRomanEncoding" ||
          properties.baseEncodingName === "WinAnsiEncoding")
      ) {
        baseEncoding = getEncoding(properties.baseEncodingName);
      }

      // If the font has an encoding and is not symbolic then follow the rules
      // in section 9.6.6.4 of the spec on how to map 3,1 and 1,0 cmaps.
      if (
        properties.hasEncoding &&
        !this.isSymbolicFont &&
        ((cmapPlatformId === 3 && cmapEncodingId === 1) ||
          (cmapPlatformId === 1 && cmapEncodingId === 0))
      ) {
        const glyphsUnicodeMap = getGlyphsUnicode();
        for (let charCode = 0; charCode < 256; charCode++) {
          let glyphName;
          if (this.differences[charCode] !== undefined) {
            glyphName = this.differences[charCode];
          } else if (baseEncoding.length && baseEncoding[charCode] !== "") {
            glyphName = baseEncoding[charCode];
          } else {
            glyphName = StandardEncoding[charCode];
          }
          if (!glyphName) {
            continue;
          }
          // Ensure that non-standard glyph names are resolved to valid ones.
          const standardGlyphName = recoverGlyphName(
            glyphName,
            glyphsUnicodeMap
          );

          let unicodeOrCharCode;
          if (cmapPlatformId === 3 && cmapEncodingId === 1) {
            unicodeOrCharCode = glyphsUnicodeMap[standardGlyphName];
          } else if (cmapPlatformId === 1 && cmapEncodingId === 0) {
            // TODO: the encoding needs to be updated with mac os table.
            unicodeOrCharCode = MacRomanEncoding.indexOf(standardGlyphName);
          }

          if (unicodeOrCharCode === undefined) {
            // Not a valid glyph name, fallback to using the /ToUnicode map
            // when no post-table exists (fixes issue13316_reduced.pdf).
            if (
              !properties.glyphNames &&
              properties.hasIncludedToUnicodeMap &&
              !(this.toUnicode instanceof IdentityToUnicodeMap)
            ) {
              const unicode = this.toUnicode.get(charCode);
              if (unicode) {
                unicodeOrCharCode = unicode.codePointAt(0);
              }
            }

            if (unicodeOrCharCode === undefined) {
              continue; // No valid glyph mapping found.
            }
          }

          for (const mapping of cmapMappings) {
            if (mapping.charCode !== unicodeOrCharCode) {
              continue;
            }
            charCodeToGlyphId[charCode] = mapping.glyphId;
            break;
          }
        }
      } else if (cmapPlatformId === 0) {
        // Default Unicode semantics, use the charcodes as is.
        for (const mapping of cmapMappings) {
          charCodeToGlyphId[mapping.charCode] = mapping.glyphId;
        }
        // Always prefer the BaseEncoding/Differences arrays, when they exist
        // (fixes issue13433.pdf).
        forcePostTable = true;
      } else if (cmapPlatformId === 3 && cmapEncodingId === 0) {
        // When a (3, 0) cmap table is present, it is used instead but the
        // spec has special rules for char codes in the range of 0xF000 to
        // 0xF0FF and it says the (3, 0) table should map the values from
        // the (1, 0) table by prepending 0xF0 to the char codes. To reverse
        // this, the upper bits of the char code are cleared, but only for the
        // special range since some PDFs have char codes outside of this range
        // (e.g. 0x2013) which when masked would overwrite other values in the
        // cmap.
        for (const mapping of cmapMappings) {
          let charCode = mapping.charCode;
          if (charCode >= 0xf000 && charCode <= 0xf0ff) {
            charCode &= 0xff;
          }
          charCodeToGlyphId[charCode] = mapping.glyphId;
        }
      } else {
        // When there is only a (1, 0) cmap table, the char code is a single
        // byte and it is used directly as the char code.
        for (const mapping of cmapMappings) {
          charCodeToGlyphId[mapping.charCode] = mapping.glyphId;
        }
      }

      // Last, try to map any missing charcodes using the post table.
      if (
        properties.glyphNames &&
        (baseEncoding.length || this.differences.length)
      ) {
        for (let i = 0; i < 256; ++i) {
          if (!forcePostTable && charCodeToGlyphId[i] !== undefined) {
            continue;
          }
          const glyphName = this.differences[i] || baseEncoding[i];
          if (!glyphName) {
            continue;
          }
          const glyphId = properties.glyphNames.indexOf(glyphName);
          if (glyphId > 0 && hasGlyph(glyphId)) {
            charCodeToGlyphId[i] = glyphId;
          }
        }
      }
    }

    if (charCodeToGlyphId.length === 0) {
      // defines at least one glyph
      charCodeToGlyphId[0] = 0;
    }

    // Typically glyph 0 is duplicated and the mapping must be updated, but if
    // there isn't enough room to duplicate, the glyph id is left the same. In
    // this case, glyph 0 may not work correctly, but that is better than
    // having the whole font fail.
    let glyphZeroId = numGlyphsOut - 1;
    if (!dupFirstEntry) {
      glyphZeroId = 0;
    }

    // When `cssFontInfo` is set, the font is used to render text in the HTML
    // view (e.g. with Xfa) so nothing must be moved in the private use area.
    if (!properties.cssFontInfo) {
      // Converting glyphs and ids into font's cmap table
      const newMapping = adjustMapping(
        charCodeToGlyphId,
        hasGlyph,
        glyphZeroId,
        this.toUnicode
      );
      this.toFontChar = newMapping.toFontChar;
      tables.cmap = {
        tag: "cmap",
        data: createCmapTable(
          newMapping.charCodeToGlyphId,
          newMapping.toUnicodeExtraMap,
          numGlyphsOut
        ),
      };

      if (!tables["OS/2"] || !validateOS2Table(tables["OS/2"], font)) {
        tables["OS/2"] = {
          tag: "OS/2",
          data: createOS2Table(
            properties,
            newMapping.charCodeToGlyphId,
            metricsOverride
          ),
        };
      }
    }

    if (!isTrueType) {
      try {
        // Trying to repair CFF file
        cffFile = new Stream(tables["CFF "].data);
        const parser = new CFFParser(
          cffFile,
          properties,
          SEAC_ANALYSIS_ENABLED
        );
        cff = parser.parse();
        cff.duplicateFirstGlyph();
        const compiler = new CFFCompiler(cff);
        tables["CFF "].data = compiler.compile();
      } catch {
        warn("Failed to compile font " + properties.loadedName);
      }
    }

    // Re-creating 'name' table
    if (!tables.name) {
      tables.name = {
        tag: "name",
        data: createNameTable(this.name),
      };
    } else {
      // ... using existing 'name' table as prototype
      const [namePrototype, nameRecords] = readNameTable(tables.name);

      tables.name.data = createNameTable(name, namePrototype);
      this.psName = namePrototype[0][6] || null;

      if (!properties.composite) {
        // For TrueType fonts that do not include `ToUnicode` or `Encoding`
        // data, attempt to use the name-table to improve text selection.
        adjustTrueTypeToUnicode(properties, this.isSymbolicFont, nameRecords);
      }
    }

    const builder = new OpenTypeFileBuilder(header.version);
    for (const tableTag in tables) {
      builder.addTable(tableTag, tables[tableTag].data);
    }
    return builder.toArray();
  }