class HexaPDF::Font::Type1::AFMParser

whitespace characters.
front of the line and then remove the parsed part from the line, including trailing
This parser reads in line by line and the type parsing functions parse a value from the
the line.
(space, newline, tab) except for the string type which just uses everything until the end of
(string, name, number, integer, array, boolean) which are separated by whitespace characters
AFM is a line oriented format. Each line consists of one or more values of supported types
== How Parsing Works
Font Metrics File Format Specification Version 4.1, available at the Adobe website.
For information on the AFM file format have a look at Adobe technical note #5004 - Adobe
adaptable to other AFM files.
AFM files for the 14 PDF core fonts is implemented. However, if need be it should be
Note that this implementation isn’t a full AFM parser, only what is needed for parsing the
Parses files in the AFM file format.

def self.parse(source)

Parses the IO or file and returns a FontMetrics object.

Parser.parse(io) -> font_metrics
Parser.parse(filename) -> font_metrics
:call-seq:
def self.parse(source)
  if source.respond_to?(:read)
    new(source).parse
  else
    File.open(source) {|file| new(file).parse }
  end
end

def each_line

internal buffer.
Iterates over all the lines in the IO, yielding every time a line has been read into the
def each_line
  read_line
  unless parse_name == 'StartFontMetrics'
    raise HexaPDF::Error, "The AFM file has to start with StartFontMetrics, not #{@line}"
  end
  until @io.eof?
    read_line
    yield
  end
end

def initialize(io)

Creates a new parse for the given IO stream.
def initialize(io)
  @io = io
end

def parse

Parses the AFM file and returns a FontMetrics object.
def parse
  @metrics = FontMetrics.new
  sections = []
  each_line do
    case (command = parse_name)
    when /\AStart/
      sections.push(command)
      case command
      when 'StartCharMetrics' then parse_character_metrics
      when 'StartKernPairs' then parse_kerning_pairs
      end
    when /\AEnd/
      sections.pop
      break if sections.empty? && command == 'EndFontMetrics'
    else
      if sections.empty?
        parse_global_font_information(command.to_sym)
      end
    end
  end
  if @metrics.bounding_box && !@metrics.descender
    @metrics.descender = @metrics.bounding_box[1]
  end
  if @metrics.bounding_box && !@metrics.ascender
    @metrics.ascender = @metrics.bounding_box[3]
  end
  @metrics
end

def parse_boolean

Parses the boolean at the start of the line.
def parse_boolean
  parse_name == 'true'
end

def parse_character_metrics

It is assumed that the StartCharMetrics name has already been parsed from the line.

Parses the character metrics in a StartCharMetrics section.
def parse_character_metrics
  parse_integer.times do
    read_line
    char = CharacterMetrics.new
    if @line =~ /C (\S+) ; WX (\S+) ; N (\S+) ; B (\S+) (\S+) (\S+) (\S+) ;((?: L \S+ \S+ ;)+)?/
      char.code = $1.to_i
      char.width = $2.to_f
      char.name = $3.to_sym
      char.bbox = [$4.to_i, $5.to_i, $6.to_i, $7.to_i]
      if $8
        @metrics.ligature_pairs[char.name] = {}
        $8.scan(/L (\S+) (\S+)/).each do |name, ligature|
          @metrics.ligature_pairs[char.name][name.to_sym] = ligature.to_sym
        end
      end
    end
    @metrics.character_metrics[char.name] = char if char.name
    @metrics.character_metrics[char.code] = char if char.code != -1
  end
end

def parse_global_font_information(command)

fonts' AFM files don't have an extra StartDirection section.
Note that writing direction metrics are also processed here since the standard 14 core

It is assumed that the command name has already been parsed from the line.

Parses global font information line for the given +command+ (a symbol).
def parse_global_font_information(command)
  case command
  when :FontName then @metrics.font_name = parse_string
  when :FullName then @metrics.full_name = parse_string
  when :FamilyName then @metrics.family_name = parse_string
  when :CharacterSet then @metrics.character_set = parse_string
  when :EncodingScheme then @metrics.encoding_scheme = parse_string
  when :Weight then @metrics.weight = parse_string
  when :FontBBox
    @metrics.bounding_box = [parse_number, parse_number, parse_number, parse_number]
  when :CapHeight then @metrics.cap_height = parse_number
  when :XHeight then @metrics.x_height = parse_number
  when :Ascender then @metrics.ascender = parse_number
  when :Descender then @metrics.descender = parse_number
  when :StdHW then @metrics.dominant_horizontal_stem_width = parse_number
  when :StdVW then @metrics.dominant_vertical_stem_width = parse_number
  when :UnderlinePosition then @metrics.underline_position = parse_number
  when :UnderlineThickness then @metrics.underline_thickness = parse_number
  when :ItalicAngle then @metrics.italic_angle = parse_number
  when :IsFixedPitch then @metrics.is_fixed_pitch = parse_boolean
  end
end

def parse_integer

Parses the integer at the start of the line.
def parse_integer
  parse_name.to_i
end

def parse_kerning_pairs

It is assumed that the StartKernPairs name has already been parsed from the line.

Parses the kerning pairs in a StartKernPairs section.
def parse_kerning_pairs
  parse_integer.times do
    read_line
    if @line =~ /KPX (\S+) (\S+) (\S+)/
      (@metrics.kerning_pairs[$1.to_sym] ||= {})[$2.to_sym] = $3.to_i
    end
  end
end

def parse_name

Parses and returns the name at the start of the line, with whitespace stripped.
def parse_name
  result = @line[/\S+\s*/].to_s
  @line[0, result.size] = ''
  result.strip!
  result
end

def parse_number

Parses the float number at the start of the line.
def parse_number
  parse_name.to_f
end

def parse_string

Returns the rest of the line, with whitespace stripped.
def parse_string
  @line.strip!
  line = @line
  @line = ''
  line
end

def read_line

Reads the next line into the current line variable.
def read_line
  @line = @io.readline
end