class TTFunk::Table::OS2

Constants

CODEPOINT_SPACE
CODE_PAGE_BITS
LOWERCASE_COUNT
LOWERCASE_END
LOWERCASE_START
SPACE_GLYPH_MISSING_ERROR
UNICODE_BLOCKS
UNICODE_MAX
UNICODE_RANGES
WEIGHT_LOWERCASE
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]
ave_char_width[R]
break_char[R]
cap_height[R]
char_range[R]
code_page_range[R]
default_char[R]
descent[R]
family_class[R]
first_char_index[R]
last_char_index[R]
line_gap[R]
max_context[R]
panose[R]
selection[R]
type[R]
vendor_id[R]
version[R]
weight_class[R]
width_class[R]
win_ascent[R]
win_descent[R]
x_height[R]
y_strikeout_position[R]
y_strikeout_size[R]
y_subscript_x_offset[R]
y_subscript_x_size[R]
y_subscript_y_offset[R]
y_subscript_y_size[R]
y_superscript_x_offset[R]
y_superscript_x_size[R]
y_superscript_y_offset[R]
y_superscript_y_size[R]

Public Class Methods

encode(os2, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 285
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 394
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 402
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 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 420
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 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 355
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 381
def group_original_code_points_by_bit(os2)
  Hash.new { |h, k| h[k] = [] }.tap do |result|
    os2.file.cmap.unicode.first.code_map.each_key do |code_point|
      # find corresponding bit
      range = UNICODE_RANGES.find { |r| r.cover?(code_point) }

      if (bit = UNICODE_BLOCKS[range])
        result[bit] << code_point
      end
    end
  end
end
unicode_blocks_for(os2, original_field, subset) click to toggle source
# File lib/ttfunk/table/os2.rb, line 363
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
# File lib/ttfunk/table/os2.rb, line 280
def tag
  'OS/2'
end

Private Instance Methods

parse!() click to toggle source
# File lib/ttfunk/table/os2.rb, line 473
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