patchFontAscentDescent function

ByteData patchFontAscentDescent(
  1. ByteData bytes,
  2. double ascentEm,
  3. double descentEm, {
  4. int? unitsPerEm,
})

bytes with the ascent and descent ascentEm and descentEm, in em, and no line gap. Returns bytes if it is not a TrueType or OpenType file with head and hhea tables.

With unitsPerEm, also writes head.unitsPerEm: the size adjust (docs/design/ADR_font_size_adjust.md in telosnex, D1). Every glyph is in font units, so the whole font scales by the old over the new value. ascentEm and descentEm are then in the new em.

Implementation

ByteData patchFontAscentDescent(
  ByteData bytes,
  double ascentEm,
  double descentEm, {
  int? unitsPerEm,
}) {
  final tables = fontTableRecords(bytes);
  if (tables == null) return bytes;
  final out = ByteData(bytes.lengthInBytes)
    ..buffer.asUint8List().setAll(
      0,
      bytes.buffer.asUint8List(bytes.offsetInBytes, bytes.lengthInBytes),
    );
  final records = {for (final e in tables.entries) e.key: e.value.record};
  int offsetOf(String tag) => tables[tag]!.offset;
  int lengthOf(String tag) => tables[tag]!.length;
  if (!records.containsKey('head') ||
      !records.containsKey('hhea') ||
      lengthOf('head') < 54 ||
      lengthOf('hhea') < 36) {
    return bytes;
  }
  final head = offsetOf('head');
  if (unitsPerEm != null) {
    if (unitsPerEm < 16 || unitsPerEm > 16384) {
      throw ArgumentError.value(unitsPerEm, 'unitsPerEm', 'not 16 to 16384');
    }
    out.setUint16(head + 18, unitsPerEm);
  }
  final em = out.getUint16(head + 18);
  // Ceil, so that the box never cuts the ink.
  final ascent = (ascentEm * em).ceil().clamp(0, 0x7fff);
  final descent = (descentEm * em).ceil().clamp(0, 0x7fff);

  final hhea = offsetOf('hhea');
  out
    ..setInt16(hhea + 4, ascent)
    ..setInt16(hhea + 6, -descent)
    ..setInt16(hhea + 8, 0);
  if (records.containsKey('OS/2') && lengthOf('OS/2') >= 78) {
    final os2 = offsetOf('OS/2');
    out
      ..setInt16(os2 + 68, ascent)
      ..setInt16(os2 + 70, -descent)
      ..setInt16(os2 + 72, 0)
      ..setUint16(os2 + 74, ascent)
      ..setUint16(os2 + 76, descent);
  }

  int checksum(int offset, int length) {
    var sum = 0;
    final end = offset + length;
    for (var i = offset; i < end; i += 4) {
      var word = 0;
      for (var b = 0; b < 4; b++) {
        word = (word << 8) | (i + b < end ? out.getUint8(i + b) : 0);
      }
      sum = (sum + word) & 0xffffffff;
    }
    return sum;
  }

  out.setUint32(head + 8, 0);
  for (final tag in records.keys) {
    if (tag == 'head' || tag == 'hhea' || tag == 'OS/2') {
      out.setUint32(records[tag]! + 4, checksum(offsetOf(tag), lengthOf(tag)));
    }
  }
  final total = checksum(0, out.lengthInBytes);
  out.setUint32(head + 8, (0xB1B0AFBA - total) & 0xffffffff);
  return out;
}