class TTFunk::Table::OS2

OS/2 and Windows Metrics (‘OS/2`) table

Constants

CODEPOINT_SPACE

Space character code point.

CODE_PAGE_BITS

Code page bits.

LOWERCASE_COUNT

Number of chracters for average character width calculation.

LOWERCASE_END

End chracter for average character width calculation.

LOWERCASE_START

Start chracter for average character width calculation.

SPACE_GLYPH_MISSING_ERROR

Error message for missing space character.

UNICODE_BLOCKS

Unicode blocks.

UNICODE_MAX

Indicates that font supports supplementary characters.

UNICODE_RANGES

Unicode block ranges.

WEIGHT_LOWERCASE

chracter weights for average character width calculation.

WEIGHT_SPACE

Used to calculate the xAvgCharWidth field. From docs.microsoft.com/en-us/typography/opentype/spec/os2:

“When first defined, the specification was biased toward Basic Latin characters, and it was thought that the xAvgCharWidth value could be used to estimate the average length of lines of text. A formula for calculating xAvgCharWidth was provided using frequency-of-use weighting factors for lowercase letters a - z.”

The array below contains 26 weight values which correspond to the 26 letters in the Latin alphabet. Each weight is the relative frequency of that letter in the English language.

Attributes

ascent[R]

The typographic ascender for this font. @return [Integer]

ave_char_width[R]

Average weighted escapement. @return [Integer]

break_char[R]

The Unicode code point, in UTF-16 encoding, of a character that can be used as a default break character. @return [Integer]

cap_height[R]

The distance between the baseline and the approximate height of uppercase letters. @return [Integer]

char_range[R]

Unicode Character Range. @return [Integer]

code_page_range[R]

Code Page Character Range. @return [Integer]

default_char[R]

The Unicode code point, in UTF-16 encoding, of a character that can be used for a default glyph if a requested character is not supported in the font. @return [Integer]

descent[R]

The typographic descender for this font. @return [Integer]

family_class[R]

Font-family class and subclass. @return [Integer]

first_char_index[R]

The minimum Unicode index (character code) in this font. @return [Integer]

last_char_index[R]

The maximum Unicode index (character code) in this font. @return [Integer]

line_gap[R]

The typographic line gap for this font. @return [Integer]

max_context[R]

The maximum length of a target glyph context for any feature in this font. @return [Integer]

panose[R]

PANOSE classification number. @return [Integer]

selection[R]

Font selection flags. @return [Integer]

type[R]

Type flags. @return [Integer]

vendor_id[R]

Font Vendor Identification. @return [String]

version[R]

Table version. @return [Integer]

weight_class[R]

Weight class. @return [Integer]

width_class[R]

Width class. @return [Integer]

win_ascent[R]

The “Windows ascender” metric. @return [Integer]

win_descent[R]

The “Windows descender” metric. @return [Integer]

x_height[R]

The distance between the baseline and the approximate height of non-ascending lowercase letters. @return [Integer]

y_strikeout_position[R]

Strikeout position. @return [Integer]

y_strikeout_size[R]

Strikeout size. @return [Integer]

y_subscript_x_offset[R]

Subscript x offset. @return [Integer]

y_subscript_x_size[R]

Subscript horizontal font size. @return [Integer]

y_subscript_y_offset[R]

Subscript y offset. @return [Integer]

y_subscript_y_size[R]

Subscript vertical font size. @return [Integer]

y_superscript_x_offset[R]

Superscript x offset. @return [Integer]

y_superscript_x_size[R]

Superscript horizontal font size. @return [Integer]

y_superscript_y_offset[R]

Superscript y offset. @return [Integer]

y_superscript_y_size[R]

Superscript vertical font size. @return [Integer]

Public Class Methods

encode(os2, subset) click to toggle source

Encode table.

@param os2 [TTFunk::Table::OS2] @param subset [TTFunk::Subset::MacRoman, TTFunk::Subset::Windows1252,

TTFunk::Subset::Unicode, TTFunk::Subset::Unicode8Bit]

@return [String]

# File lib/ttfunk/table/os2.rb, line 411
def encode(os2, subset)
  result = ''.b
  result << [
    os2.version, avg_char_width_for(os2, subset), os2.weight_class,
    os2.width_class, os2.type, os2.y_subscript_x_size,
    os2.y_subscript_y_size, os2.y_subscript_x_offset,
    os2.y_subscript_y_offset, os2.y_superscript_x_size,
    os2.y_superscript_y_size, os2.y_superscript_x_offset,
    os2.y_superscript_y_offset, os2.y_strikeout_size,
    os2.y_strikeout_position, os2.family_class,
  ].pack('n*')

  result << os2.panose

  new_char_range = unicode_blocks_for(os2, os2.char_range, subset)
  result << BinUtils
    .slice_int(
      new_char_range.value,
      bit_width: 32,
      slice_count: 4,
    )
    .pack('N*')

  result << os2.vendor_id

  new_cmap_table = subset.new_cmap_table[:charmap]
  code_points = new_cmap_table
    .keys
    .select { |k| (new_cmap_table[k][:new]).positive? }
    .sort

  # "This value depends on which character sets the font supports.
  # This field cannot represent supplementary character values
  # (codepoints greater than 0xFFFF). Fonts that support
  # supplementary characters should set the value in this field
  # to 0xFFFF."
  first_char_index = [code_points.first || 0, UNICODE_MAX].min
  last_char_index = [code_points.last || 0, UNICODE_MAX].min

  result << [
    os2.selection, first_char_index, last_char_index,
  ].pack('n*')

  if os2.version.positive?
    result << [
      os2.ascent, os2.descent, os2.line_gap,
      os2.win_ascent, os2.win_descent,
    ].pack('n*')

    result << BinUtils
      .slice_int(
        code_pages_for(subset).value,
        bit_width: 32,
        slice_count: 2,
      )
      .pack('N*')

    if os2.version > 1
      result << [
        os2.x_height, os2.cap_height, os2.default_char,
        os2.break_char, os2.max_context,
      ].pack('n*')
    end
  end

  result
end

Private Class Methods

avg_char_width_for(os2, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 522
def avg_char_width_for(os2, subset)
  if subset.microsoft_symbol?
    avg_ms_symbol_char_width_for(os2, subset)
  else
    avg_weighted_char_width_for(os2, subset)
  end
end
avg_ms_symbol_char_width_for(os2, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 530
def avg_ms_symbol_char_width_for(os2, subset)
  total_width = 0
  num_glyphs = 0

  # use new -> old glyph mapping in order to include compound glyphs
  # in the calculation
  subset.new_to_old_glyph.each_value do |old_gid|
    if (metric = os2.file.horizontal_metrics.for(old_gid))
      total_width += metric.advance_width
      num_glyphs += 1 if metric.advance_width.positive?
    end
  end

  return 0 if num_glyphs.zero?

  total_width / num_glyphs # this should be a whole number
end
avg_weighted_char_width_for(os2, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 548
def avg_weighted_char_width_for(os2, subset)
  # make sure the subset includes the space char
  unless subset.to_unicode_map[CODEPOINT_SPACE]
    raise SPACE_GLYPH_MISSING_ERROR
  end

  space_gid = os2.file.cmap.unicode.first[CODEPOINT_SPACE]
  space_hm = os2.file.horizontal_metrics.for(space_gid)
  return 0 unless space_hm

  total_weight = space_hm.advance_width * WEIGHT_SPACE
  num_lowercase = 0

  # calculate the weighted sum of all the lowercase widths in
  # the subset
  LOWERCASE_START.upto(LOWERCASE_END) do |lowercase_cp|
    # make sure the subset includes the character
    next unless subset.to_unicode_map[lowercase_cp]

    lowercase_gid = os2.file.cmap.unicode.first[lowercase_cp]
    lowercase_hm = os2.file.horizontal_metrics.for(lowercase_gid)

    num_lowercase += 1
    total_weight += lowercase_hm.advance_width *
      WEIGHT_LOWERCASE[lowercase_cp - 'a'.ord]
  end

  # return if all lowercase characters are present in the subset
  return total_weight / 1000 if num_lowercase == LOWERCASE_COUNT

  # If not all lowercase characters are present in the subset, take
  # the average width of all the subsetted characters. This differs
  # from avg_ms_char_width_for in that it includes zero-width glyphs
  # in the calculation.
  total_width = 0
  num_glyphs = subset.new_to_old_glyph.size

  # use new -> old glyph mapping in order to include compound glyphs
  # in the calculation
  subset.new_to_old_glyph.each_value do |old_gid|
    if (metric = os2.file.horizontal_metrics.for(old_gid))
      total_width += metric.advance_width
    end
  end

  return 0 if num_glyphs.zero?

  total_width / num_glyphs # this should be a whole number
end
code_pages_for(subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 481
def code_pages_for(subset)
  field = BitField.new(0)
  return field if subset.unicode?

  field.on(CODE_PAGE_BITS[subset.code_page])
  field
end
group_original_code_points_by_bit(os2) click to toggle source
# File lib/ttfunk/table/os2.rb, line 507
def group_original_code_points_by_bit(os2)
  Hash.new { |h, k| h[k] = [] }.tap do |result|
    code_points = os2.file.cmap.unicode.first.code_map.keys.sort
    UNICODE_RANGES.each do |r|
      code_points = code_points.drop_while { |p| p < r.min }
      code_points.take_while { |p| p <= r.max }.each do |code_point|
        if (bit = UNICODE_BLOCKS[r])
          result[bit] << code_point
        end
      end
      code_points = code_points.drop_while { |p| p <= r.max }
    end
  end
end
unicode_blocks_for(os2, original_field, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 489
def unicode_blocks_for(os2, original_field, subset)
  field = BitField.new(0)
  return field unless subset.unicode?

  subset_code_points = Set.new(subset.new_cmap_table[:charmap].keys)
  original_code_point_groups = group_original_code_points_by_bit(os2)

  original_code_point_groups.each do |bit, code_points|
    next if original_field.off?(bit)

    if code_points.any? { |cp| subset_code_points.include?(cp) }
      field.on(bit)
    end
  end

  field
end

Public Instance Methods

tag() click to toggle source

Table tag. @return [String]

# File lib/ttfunk/table/os2.rb, line 400
def tag
  'OS/2'
end

Private Instance Methods

parse!() click to toggle source
# File lib/ttfunk/table/os2.rb, line 601
def parse!
  @version = read(2, 'n').first

  @ave_char_width = read_signed(1).first
  @weight_class, @width_class = read(4, 'nn')
  @type, @y_subscript_x_size, @y_subscript_y_size, @y_subscript_x_offset,
    @y_subscript_y_offset, @y_superscript_x_size, @y_superscript_y_size,
    @y_superscript_x_offset, @y_superscript_y_offset, @y_strikeout_size,
    @y_strikeout_position, @family_class = read_signed(12)
  @panose = io.read(10)

  @char_range = BitField.new(BinUtils.stitch_int(read(16, 'N*'), bit_width: 32))

  @vendor_id = io.read(4)
  @selection, @first_char_index, @last_char_index = read(6, 'n*')

  if @version.positive?
    @ascent, @descent, @line_gap = read_signed(3)
    @win_ascent, @win_descent = read(4, 'nn')
    @code_page_range = BitField.new(BinUtils.stitch_int(read(8, 'N*'), bit_width: 32))

    if @version > 1
      @x_height, @cap_height = read_signed(2)
      @default_char, @break_char, @max_context = read(6, 'nnn')

      # Set this to zero until GSUB/GPOS support has been implemented.
      # This value is calculated via those tables, and should be set to
      # zero if the data is not available.
      @max_context = 0
    end
  end
end